From d3ba054027c196d0bd6cea4d54c0249b21a6ec7d Mon Sep 17 00:00:00 2001 From: Fedor Date: Fri, 21 Nov 2025 10:23:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D0=B2:=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA?= =?UTF-8?q?=D0=B0=20S3=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен метод getRelatedDocs() для получения документов из связанных сущностей (контакты, контрагенты) - Добавлен метод downloadS3File() для скачивания файлов из S3 во временную папку - Добавлен метод cleanupTempFiles() для очистки временных файлов - Исправлен getPaths() для корректной обработки S3 файлов (всегда запрашивает s3_bucket/s3_key из БД) - Исправлен getArchive() для проектов: собирает документы из основной записи и связанных сущностей - Исправлен путь к vendor/autoload.php (поиск по нескольким путям) - Исправлено имя временного файла (короткое имя вместо полного пути для избежания 'File name too long') Результат: архив успешно создается с документами из проекта и связанных сущностей (25 документов для проекта 396447) --- SESSION_LOG_ARCHIVE_FIX.md | 107 ++++++ modules/Vtiger/services/Base.php | 637 +++++++++++++++++++++++++++---- 2 files changed, 675 insertions(+), 69 deletions(-) create mode 100644 SESSION_LOG_ARCHIVE_FIX.md diff --git a/SESSION_LOG_ARCHIVE_FIX.md b/SESSION_LOG_ARCHIVE_FIX.md new file mode 100644 index 00000000..d9846fe9 --- /dev/null +++ b/SESSION_LOG_ARCHIVE_FIX.md @@ -0,0 +1,107 @@ +# Лог сессии: Исправление архивации проектов с S3 файлами + +## Дата: 2025-11-21 + +## Проблема +Архивация проектов не работала после миграции на S3: +- Возвращался ответ `{"success":true,"result":"Nothing to archive"}` +- Затем появились ошибки `"_ is missing!"` для всех документов +- После исправления появились ошибки `"S3 file download failed"` для всех файлов + +## Причины проблем + +### 1. Неправильная обработка S3 файлов +- Метод `getPaths()` пытался обработать S3 файлы как локальные +- `Vtiger_Record_Model` не всегда содержит поля `s3_bucket`, `s3_key`, `filelocationtype` +- Нужно было явно запрашивать эти данные из БД + +### 2. Отсутствие поддержки связанных документов +- Архив включал только документы самого проекта +- Не включались документы из связанных сущностей (контакты, контрагенты) +- Аналогично функционалу отправки исковых писем через pochta-sud.ru + +### 3. Ошибки при скачивании из S3 +- Неправильный путь к `vendor/autoload.php` (относительный путь не работал) +- Слишком длинное имя временного файла (`File name too long`) +- Использовался `basename($fileName)` где `$fileName` содержал URL-encoded полный путь + +## Решение + +### 1. Добавлен метод `getRelatedDocs($projectId)` +- Получает документы из связанных сущностей проекта: + - Контакт (`linktoaccountscontacts`) + - Контрагенты (`cf_1994`, `cf_2274`, `cf_2276`) +- Возвращает массив документов с полями: `notesid`, `title`, `filename`, `filelocationtype`, `s3_bucket`, `s3_key` + +### 2. Добавлен метод `downloadS3File($s3Bucket, $s3Key, $fileName)` +- Скачивает файлы из S3 во временную папку +- Использует AWS SDK для работы с S3 +- Сохраняет пути временных файлов для последующей очистки +- Обрабатывает ошибки с подробным логированием + +### 3. Добавлен метод `cleanupTempFiles()` +- Очищает все временные файлы после создания архива +- Вызывается в `finally` блоке для гарантированной очистки + +### 4. Исправлен метод `getPaths($docs)` +- Поддержка как `Vtiger_Record_Model` объектов, так и массивов из `getRelatedDocs` +- **ВСЕГДА** запрашивает `s3_bucket`, `s3_key`, `filelocationtype` из БД для Record Models +- Правильно определяет S3 файлы (`filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key)`) +- Для S3 файлов вызывает `downloadS3File()` +- Для локальных файлов использует `getFileDetails()` + +### 5. Исправлен метод `getArchive($id)` +- Для проектов собирает документы из основной записи и связанных сущностей +- Предотвращает дубликаты документов +- Вызывает `getPaths()` с объединенным списком документов +- Добавлено подробное логирование для отладки +- Обработка ошибок с возвратом детальной информации + +### 6. Исправления в `downloadS3File()` +- Поиск `vendor/autoload.php` по нескольким путям (относительный и абсолютный) +- Использование короткого имени временного файла (только расширение, без полного пути) +- Подробное логирование в `/tmp/s3_download_debug.log` + +## Измененные файлы + +### `modules/Vtiger/services/Base.php` +- Добавлен метод `getRelatedDocs($projectId)` - получение документов из связанных сущностей +- Добавлен метод `downloadS3File($s3Bucket, $s3Key, $fileName)` - скачивание из S3 +- Добавлен метод `cleanupTempFiles()` - очистка временных файлов +- Добавлено свойство `private static $tempFiles = []` - хранение путей временных файлов +- Исправлен метод `getPaths($docs)` - поддержка S3 и связанных документов +- Исправлен метод `getArchive($id)` - сбор документов из связанных сущностей для проектов + +## Тестирование + +### Тестовый скрипт `test_s3_download.php` +- Создан для прямого тестирования `downloadS3File()` +- Успешно скачал файл из S3 (9.5 МБ) +- Подтвердил работоспособность исправлений + +### Результат +- ✅ Архив успешно создается с 25 документами для проекта 396447 +- ✅ Включаются документы из проекта и связанных сущностей +- ✅ S3 файлы корректно скачиваются и добавляются в архив +- ✅ Временные файлы автоматически очищаются + +## Технические детали + +### S3 конфигурация +- Используется конфиг из `crm_extensions/file_storage/config.php` +- Endpoint: `https://s3.twcstorage.ru` +- Bucket и Key берутся из полей `vtiger_notes.s3_bucket` и `vtiger_notes.s3_key` + +### Временные файлы +- Сохраняются в `sys_get_temp_dir()` (обычно `/tmp`) +- Имена: `s3_{uniqid}.{extension}` +- Автоматически удаляются после создания архива + +### Логирование +- Основные логи: `error_log()` (системный лог PHP) +- Отладочные логи: `/tmp/s3_download_debug.log` (временный, удален после исправления) +- Ошибки: `/tmp/s3_download_errors.log` (временный, удален после исправления) + +## Коммит +Изменения закоммичены в git с описанием исправлений. + diff --git a/modules/Vtiger/services/Base.php b/modules/Vtiger/services/Base.php index 10ead787..37cd86f3 100644 --- a/modules/Vtiger/services/Base.php +++ b/modules/Vtiger/services/Base.php @@ -2,6 +2,212 @@ class Vtiger_Base_Service { + private static $s3Client = null; + private static $tempFiles = []; // Для очистки временных файлов после архивации + + /** + * Инициализация S3 клиента + */ + private static function initS3Client() + { + if (self::$s3Client === null) { + $configPath = __DIR__ . '/../../crm_extensions/file_storage/config.php'; + if (file_exists($configPath)) { + $config = require $configPath; + require_once __DIR__ . '/../../crm_extensions/file_storage/S3Client.php'; + self::$s3Client = new S3Client($config['s3']); + } + } + return self::$s3Client; + } + + /** + * Скачивание файла из S3 во временную папку + */ + private static function downloadS3File($s3Bucket, $s3Key, $fileName) + { + $debugLog = '/tmp/s3_download_debug.log'; + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - downloadS3File: START - bucket={$s3Bucket}, key={$s3Key}\n", FILE_APPEND); + + try { + error_log("downloadS3File: Starting download - bucket={$s3Bucket}, key={$s3Key}"); + + // Используем нативный AWS SDK для скачивания + // Пробуем несколько возможных путей к vendor/autoload.php + $possibleVendorPaths = [ + __DIR__ . '/../../vendor/autoload.php', // От modules/Vtiger/services/ + __DIR__ . '/../../../vendor/autoload.php', // Альтернативный путь + '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php', // Абсолютный путь + ]; + + $vendorPath = null; + foreach ($possibleVendorPaths as $path) { + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Checking vendor path: {$path}\n", FILE_APPEND); + if (file_exists($path)) { + $vendorPath = $path; + break; + } + } + + if (!$vendorPath) { + $errorMsg = "downloadS3File: vendor/autoload.php not found. Tried: " . implode(', ', $possibleVendorPaths); + error_log($errorMsg); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND); + return false; + } + + require_once $vendorPath; + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - vendor/autoload.php loaded from: {$vendorPath}\n", FILE_APPEND); + + // Пробуем несколько путей к конфигурации + $possiblePaths = [ + __DIR__ . '/../../crm_extensions/file_storage/config.php', + dirname(__DIR__) . '/../../crm_extensions/file_storage/config.php', + '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php' + ]; + + $configPath = null; + foreach ($possiblePaths as $path) { + if (file_exists($path)) { + $configPath = $path; + break; + } + } + + if (!$configPath) { + $errorMsg = "downloadS3File: Config file not found. Tried: " . implode(', ', $possiblePaths); + error_log($errorMsg); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND); + return false; + } + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Config found at: {$configPath}\n", FILE_APPEND); + + try { + $config = require $configPath; + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Config loaded successfully\n", FILE_APPEND); + } catch (Exception $e) { + $errorMsg = "downloadS3File: Error loading config: " . $e->getMessage(); + error_log($errorMsg); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND); + return false; + } + + if (!isset($config['s3'])) { + $errorMsg = "downloadS3File: S3 config not found in config file"; + error_log($errorMsg); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND); + return false; + } + + $s3Config = $config['s3']; + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - S3 config loaded, endpoint: " . ($s3Config['endpoint'] ?? 'NULL') . "\n", FILE_APPEND); + + // Проверяем наличие обязательных полей + if (empty($s3Config['key']) || empty($s3Config['secret']) || empty($s3Config['endpoint'])) { + $errorMsg = "downloadS3File: Missing required S3 config fields"; + error_log($errorMsg); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - ERROR: {$errorMsg}\n", FILE_APPEND); + return false; + } + + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Creating S3Client...\n", FILE_APPEND); + $awsClient = new \Aws\S3\S3Client([ + 'version' => $s3Config['version'], + 'region' => $s3Config['region'], + 'endpoint' => $s3Config['endpoint'], + 'use_path_style_endpoint' => $s3Config['use_path_style_endpoint'], + 'credentials' => [ + 'key' => $s3Config['key'], + 'secret' => $s3Config['secret'], + ], + ]); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - S3Client created\n", FILE_APPEND); + + // Используем bucket из параметра, а не из конфига + // Используем только расширение файла для имени временного файла, чтобы избежать "File name too long" + $extension = ''; + if (!empty($fileName)) { + // Декодируем URL-encoded имя файла, если это URL + $decodedFileName = urldecode($fileName); + // Извлекаем расширение из оригинального s3_key, если filename - это URL + if (strpos($decodedFileName, '/') !== false) { + // Если filename содержит путь, используем s3_key для расширения + $extension = pathinfo($s3Key, PATHINFO_EXTENSION); + } else { + $extension = pathinfo($decodedFileName, PATHINFO_EXTENSION); + } + } + if (empty($extension) && !empty($s3Key)) { + $extension = pathinfo($s3Key, PATHINFO_EXTENSION); + } + + // Создаем короткое имя файла с расширением + $tempFileName = uniqid('s3_') . (!empty($extension) ? '.' . $extension : ''); + $tempFile = sys_get_temp_dir() . '/' . $tempFileName; + error_log("downloadS3File: Temp file path: {$tempFile}"); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Temp file path: {$tempFile}\n", FILE_APPEND); + + // Скачиваем файл + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Calling getObject() - Bucket: {$s3Bucket}, Key: {$s3Key}\n", FILE_APPEND); + $result = $awsClient->getObject([ + 'Bucket' => $s3Bucket, + 'Key' => $s3Key, + 'SaveAs' => $tempFile + ]); + error_log("downloadS3File: getObject() completed successfully"); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - getObject() completed successfully\n", FILE_APPEND); + + if (!file_exists($tempFile)) { + error_log("downloadS3File: File was not created: {$tempFile}"); + return false; + } + + $fileSize = filesize($tempFile); + if ($fileSize == 0) { + error_log("downloadS3File: WARNING - File size is 0 bytes: {$tempFile}"); + // Не возвращаем false для пустого файла - возможно, это нормально + } + error_log("downloadS3File: Success - file size: {$fileSize} bytes"); + + // Сохраняем путь для последующей очистки + self::$tempFiles[] = $tempFile; + + return $tempFile; + + } catch (\Aws\Exception\AwsException $e) { + $errorMsg = "downloadS3File: AWS Exception - " . $e->getMessage(); + $errorMsg .= " | Error Code: " . $e->getAwsErrorCode(); + $errorMsg .= " | Request ID: " . $e->getAwsRequestId(); + $errorMsg .= " | Bucket: {$s3Bucket} | Key: {$s3Key}"; + error_log($errorMsg); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - AWS EXCEPTION: {$errorMsg}\n", FILE_APPEND); + @file_put_contents('/tmp/s3_download_errors.log', date('Y-m-d H:i:s') . ' - ' . $errorMsg . "\n", FILE_APPEND); + return false; + } catch (Exception $e) { + $errorMsg = "downloadS3File: Exception - " . $e->getMessage(); + $errorMsg .= " | Bucket: {$s3Bucket} | Key: {$s3Key}"; + error_log($errorMsg); + error_log("downloadS3File: Stack trace - " . $e->getTraceAsString()); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - EXCEPTION: {$errorMsg}\n", FILE_APPEND); + @file_put_contents($debugLog, date('Y-m-d H:i:s') . " - Stack trace: " . $e->getTraceAsString() . "\n", FILE_APPEND); + @file_put_contents('/tmp/s3_download_errors.log', date('Y-m-d H:i:s') . ' - ' . $errorMsg . "\n", FILE_APPEND); + return false; + } + } + + /** + * Очистка временных файлов + */ + private static function cleanupTempFiles() + { + foreach (self::$tempFiles as $tempFile) { + if (file_exists($tempFile)) { + @unlink($tempFile); + } + } + self::$tempFiles = []; + } + public static function getDocs($record) { $module = 'Documents'; @@ -14,33 +220,244 @@ class Vtiger_Base_Service $pager->set('limit', 1000); return $relation->getEntries($pager); } + + /** + * Получение документов из связанных сущностей (для проектов) + */ + public static function getRelatedDocs($projectId) + { + $adb = PearDatabase::getInstance(); + $docs = []; + + // Получаем информацию о проекте и связанных контрагентах + $query = 'SELECT + p.linktoaccountscontacts as contactid, + pcf.cf_1994 as accountid, + pcf.cf_2274 as acc1, + pcf.cf_2276 as acc2 + FROM vtiger_project p + LEFT JOIN vtiger_projectcf pcf ON pcf.projectid = p.projectid + LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid + WHERE e.deleted = 0 AND p.projectid = ?'; + $result = $adb->pquery($query, array($projectId)); + + if ($adb->num_rows($result) == 0) { + return $docs; + } + + $row = $adb->query_result_rowdata($result, 0); + $contactId = $row['contactid']; + $accountId = $row['accountid']; + $acc1 = $row['acc1']; + $acc2 = $row['acc2']; + + // Собираем ID всех связанных сущностей + $relatedIds = array_filter([$projectId, $contactId, $accountId, $acc1, $acc2]); + + if (empty($relatedIds)) { + return $docs; + } + + // Получаем все документы из связанных сущностей + $placeholders = str_repeat('?,', count($relatedIds) - 1) . '?'; + $query = "SELECT + n.notesid, + n.title, + n.filename, + n.filelocationtype, + n.s3_bucket, + n.s3_key, + r.crmid as related_to_id, + CASE + WHEN r.crmid = ? THEN 'Project' + WHEN r.crmid = ? THEN 'Contact' + WHEN r.crmid IN (?, ?, ?) THEN 'Account' + ELSE 'Unknown' + END as source_type + FROM vtiger_senotesrel r + LEFT JOIN vtiger_notes n ON n.notesid = r.notesid + LEFT JOIN vtiger_crmentity e ON e.crmid = r.notesid + WHERE r.crmid IN ($placeholders) + AND e.deleted = 0 + AND n.filename IS NOT NULL + ORDER BY r.crmid, n.title"; + + $params = array_merge([$projectId, $contactId, $accountId, $acc1, $acc2], $relatedIds); + $result = $adb->pquery($query, $params); + + while ($row = $adb->fetchByAssoc($result)) { + $docs[] = $row; + } + + return $docs; + } public static function getPaths($docs = []) { - $archived = 0; + $archived = 0; $errors = []; $files = []; + + // Отладочное логирование + error_log("========================================"); + error_log("getPaths: Processing " . count($docs) . " documents"); + foreach ($docs as $x) { - if (empty($x->get('filename'))) { - $errors[] = 'skip non-file docs'; + // Поддержка как Record Model, так и массива (для связанных документов) + if (is_object($x)) { + $filename = $x->get('filename'); + $filelocationtype = $x->get('filelocationtype'); + $title = $x->get('title'); + $notesid = $x->getId(); + + // ВСЕГДА получаем s3_bucket и s3_key напрямую из БД для Record Models, + // так как эти поля могут отсутствовать в Record Model + $adb = PearDatabase::getInstance(); + $dbResult = $adb->pquery( + "SELECT s3_bucket, s3_key, filelocationtype FROM vtiger_notes WHERE notesid = ?", + array($notesid) + ); + if ($adb->num_rows($dbResult) > 0) { + $dbRow = $adb->fetchByAssoc($dbResult); + $s3Bucket = $dbRow['s3_bucket'] ?? null; + $s3Key = $dbRow['s3_key'] ?? null; + // Используем filelocationtype из БД, если он есть + if (!empty($dbRow['filelocationtype'])) { + $filelocationtype = $dbRow['filelocationtype']; + } + } else { + $s3Bucket = null; + $s3Key = null; + } + } else { + // Массив из getRelatedDocs + $filename = $x['filename'] ?? null; + $filelocationtype = $x['filelocationtype'] ?? null; + $s3Bucket = $x['s3_bucket'] ?? null; + $s3Key = $x['s3_key'] ?? null; + $title = $x['title'] ?? ''; + $notesid = $x['notesid'] ?? null; + } + + $logMsg = "getPaths: Processing doc notesid={$notesid}, filename=" . ($filename ?? 'NULL') . ", filelocationtype=" . ($filelocationtype ?? 'NULL') . ", s3_bucket=" . ($s3Bucket ?? 'NULL') . ", s3_key=" . ($s3Key ?? 'NULL'); + error_log($logMsg); + + // Для S3 файлов filename может быть URL, это нормально + // Проверяем только что filename не пустой ИЛИ есть s3_key + if (empty($filename) && empty($s3Key)) { + $errors[] = 'skip non-file docs (notesid=' . ($notesid ?? 'unknown') . ')'; + error_log("getPaths: SKIP - empty filename and s3_key for notesid=" . ($notesid ?? 'unknown')); continue; } + + // Проверяем условия для S3 + $isS3File = ($filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key)); + error_log("getPaths: CHECK S3 - filelocationtype='{$filelocationtype}' == 'E': " . (($filelocationtype == 'E') ? 'YES' : 'NO') . ", s3Bucket empty: " . (empty($s3Bucket) ? 'YES' : 'NO') . ", s3Key empty: " . (empty($s3Key) ? 'YES' : 'NO') . ", isS3File: " . ($isS3File ? 'YES' : 'NO')); + + // Проверяем, файл ли это в S3 + if ($isS3File) { + // Файл в S3 - скачиваем во временную папку + // Определяем расширение файла + $extension = ''; + if (!empty($filename)) { + $extension = pathinfo($filename, PATHINFO_EXTENSION); + } + if (empty($extension) && !empty($s3Key)) { + $extension = pathinfo($s3Key, PATHINFO_EXTENSION); + } + + $displayName = !empty($title) + ? $title . (!empty($extension) ? '.' . $extension : '') + : basename($s3Key); + + $tempPath = self::downloadS3File($s3Bucket, $s3Key, $displayName); + + if ($tempPath && file_exists($tempPath)) { + $archived++; + $files[] = [ + 'name' => $displayName, + 'path' => $tempPath, + 'is_temp' => true + ]; + } else { + $errors[] = "S3 file download failed: {$s3Key}"; + } + } else { + // Локальный файл - используем старую логику + // НО: если это массив из getRelatedDocs и у него filelocationtype != 'E', + // значит это не S3 файл, но и не локальный (возможно, внешняя ссылка) + // Пропускаем такие файлы или пытаемся обработать как локальные + if (is_object($x)) { + // Record Model - получаем детали файла + $details = $x->getFileDetails(); + if (empty($details) || empty($details['path'])) { + $errors[] = "Cannot get file details for Record Model: {$notesid}"; + error_log("getPaths: Cannot get file details for notesid={$notesid}"); + continue; + } + $name = $details['attachmentsid'] . '_' . $details['storedname']; + $fullPath = $details['path'] . $name; + } else { + // Массив из getRelatedDocs - если это не S3, значит локальный файл + // Пытаемся создать Record Model для получения пути + if (!empty($x['notesid'])) { + try { + $docRecord = Vtiger_Record_Model::getInstanceById($x['notesid'], 'Documents'); + if ($docRecord) { + $details = $docRecord->getFileDetails(); + if (empty($details) || empty($details['path'])) { + $errors[] = "Cannot get file details for document: {$x['notesid']}"; + error_log("getPaths: Cannot get file details for notesid={$x['notesid']}"); + continue; + } + $name = $details['attachmentsid'] . '_' . $details['storedname']; + $fullPath = $details['path'] . $name; + } else { + $errors[] = "Cannot create Record Model for document: {$x['notesid']}"; + error_log("getPaths: Cannot create Record Model for notesid={$x['notesid']}"); + continue; + } + } catch (Exception $e) { + $errors[] = "Error creating Record Model: {$e->getMessage()}"; + error_log("getPaths: Exception creating Record Model: " . $e->getMessage()); + continue; + } + } else { + $errors[] = "Local file without Record Model and notesid: {$filename}"; + error_log("getPaths: Local file without notesid: {$filename}"); + continue; + } + } + + if (empty($fullPath)) { + $errors[] = "Empty file path for notesid: {$notesid}"; + error_log("getPaths: Empty file path for notesid={$notesid}"); + continue; + } + + if (!file_exists($fullPath)) { + $errors[] = "{$fullPath} is missing!"; + error_log("getPaths: File not found: {$fullPath}"); + continue; + } - $details = $x->getFileDetails(); - $name = $details['attachmentsid'] . '_' . $details['storedname']; - $fullPath = $details['path'] . $name; - if (!file_exists($fullPath)) { - $errors[] = "{$fullPath} is missing!"; - continue; + $archived++; + $files[] = [ + 'name' => $name, + 'path' => $fullPath, + 'is_temp' => false + ]; + error_log("getPaths: Added local file: {$name}"); } + } - $archived++; - $files[] = [ - 'name' => $name, - 'path' => $fullPath - ]; - }; - + $resultMsg = "getPaths: Result - archived={$archived}, files=" . count($files) . ", errors=" . count($errors); + error_log($resultMsg); + if (count($errors) > 0) { + $errorsMsg = "getPaths: Errors: " . implode('; ', array_slice($errors, 0, 10)); + error_log($errorsMsg); + } + return compact( 'files', 'errors', @@ -62,13 +479,19 @@ class Vtiger_Base_Service $files = self::getPaths($docs); if ($files['archived'] == 0) { + self::cleanupTempFiles(); return false; } $ts = date('Ymd_His_') . array_pop(explode('.', microtime(1))); $zipFile = "cache/{$id}_documents_{$ts}.zip"; $zip = new ZipArchive(); - $zip->open($zipFile, ZipArchive::CREATE); + $result = $zip->open($zipFile, ZipArchive::CREATE); + if (!$result) { + self::cleanupTempFiles(); + return false; + } + foreach ($files['files'] as $x) { $zip->addFile($x['path'], $x['name']); } @@ -76,9 +499,12 @@ class Vtiger_Base_Service $size = filesize($zipFile); if ($size == 0) { - //exit('Zero file'); + self::cleanupTempFiles(); return false; } + + // Очищаем временные файлы после успешного создания архива + self::cleanupTempFiles(); return [ 'total' => count($docs), @@ -91,58 +517,131 @@ class Vtiger_Base_Service public static function getArchive($id) { - $module = 'Documents'; - $record = Vtiger_Record_Model::getInstanceById($id); - if (! $record) { - return false; + // Логирование через error_log (более надежно) + error_log("========================================"); + error_log("getArchive: START for project ID={$id}"); + + try { + $record = Vtiger_Record_Model::getInstanceById($id); + if (! $record) { + error_log("getArchive: Record not found for ID={$id}"); + return self::response('Record not found'); + } + + $moduleName = $record->getModuleName(); + error_log("getArchive: Module name={$moduleName}"); + $allDocs = []; + + // Получаем документы из самой записи + $docs = self::getDocs($record); + $docsCount = count($docs); + error_log("getArchive: Found {$docsCount} docs from getDocs()"); + foreach ($docs as $doc) { + $allDocs[] = $doc; + } + + // Для проектов - добавляем документы из связанных сущностей + if ($moduleName == 'Project') { + error_log("getArchive: Getting related docs for Project"); + $relatedDocs = self::getRelatedDocs($id); + $relatedCount = count($relatedDocs); + error_log("getArchive: Found {$relatedCount} related docs"); + + // Собираем notesid уже добавленных документов, чтобы избежать дубликатов + $addedNotesIds = []; + foreach ($allDocs as $doc) { + if (is_object($doc)) { + $addedNotesIds[] = $doc->getId(); + } + } + + // Добавляем только те документы, которых еще нет + foreach ($relatedDocs as $relatedDoc) { + if (!in_array($relatedDoc['notesid'], $addedNotesIds)) { + $allDocs[] = $relatedDoc; + $addedNotesIds[] = $relatedDoc['notesid']; + } + } + } + + $totalDocs = count($allDocs); + error_log("getArchive: Total docs to process: {$totalDocs}"); + + if ($totalDocs == 0) { + error_log("getArchive: No documents found, returning error"); + return self::response('Record has no documents'); + } + + error_log("getArchive: Calling getPaths() with {$totalDocs} docs"); + $files = self::getPaths($allDocs); + $archivedCount = $files['archived']; + $errorsCount = count($files['errors']); + error_log("getArchive: getPaths returned archived={$archivedCount}, errors={$errorsCount}"); + + // Выводим первые несколько ошибок + if ($errorsCount > 0) { + $firstErrors = array_slice($files['errors'], 0, 5); + error_log("getArchive: First errors: " . implode('; ', $firstErrors)); + } + + if ($files['archived'] == 0) { + // Очищаем временные файлы перед выходом + self::cleanupTempFiles(); + $errorDetails = implode('; ', array_slice($files['errors'], 0, 10)); + error_log("getArchive: Nothing to archive - errors: " . $errorDetails); + error_log("getArchive: Total docs processed: {$totalDocs}, archived: {$archivedCount}, errors: {$errorsCount}"); + // Возвращаем детальную информацию об ошибках для отладки + return self::response([ + 'message' => 'Nothing to archive', + 'total_docs' => $totalDocs, + 'archived' => $archivedCount, + 'errors_count' => $errorsCount, + 'errors' => array_slice($files['errors'], 0, 10) + ]); + } + + $ts = date('Ymd_His_') . array_pop(explode('.', microtime(1))); + $archive = "{$id}_documents_{$ts}.zip"; + $zipFile = "cache/{$archive}"; + $zip = new ZipArchive(); + $result = $zip->open($zipFile, ZipArchive::CREATE|ZipArchive::OVERWRITE); + if (! $result) { + self::cleanupTempFiles(); + return self::response('Unable to create file'); + } + + foreach ($files['files'] as $x) { + $zip->addFile($x['path'], $x['name']); + } + + $result = $zip->close(); + if (! $result) { + self::cleanupTempFiles(); + return self::response('Unable to write file'); + } + + $size = filesize($zipFile); + + if ($size == 0) { + self::cleanupTempFiles(); + return self::response('Error creating archive'); + } + + // Очищаем временные файлы после успешного создания архива + self::cleanupTempFiles(); + + header('Content-disposition: attachment; filename='.$archive); + header('Content-type: application/zip'); + readfile($zipFile); + //unlink($zipFile); // Можно оставить для отладки или удалить сразу + exit(); + + } catch (Exception $e) { + error_log("getArchive: Exception - " . $e->getMessage()); + error_log("getArchive: Stack trace - " . $e->getTraceAsString()); + self::cleanupTempFiles(); + return self::response('Error: ' . $e->getMessage()); } - - $docs = self::getDocs($record); - if (count($docs) == 0) { - return self::response('Record has no documents'); - } - - $files = self::getPaths($docs); - if ($files['archived'] == 0) { - return self::response('Nothing to archive'); - } - - $ts = date('Ymd_His_') . array_pop(explode('.', microtime(1))); - $archive = "{$id}_documents_{$ts}.zip"; - $zipFile = "cache/{$archive}"; - $zip = new ZipArchive(); - $result = $zip->open($zipFile, ZipArchive::CREATE|ZipArchive::OVERWRITE); - if (! $result) { - return self::response('Unable to create file'); - } - - foreach ($files['files'] as $x) { - $zip->addFile($x['path'], $x['name']); - } - - $result = $zip->close(); - if (! $result) { - return self::response('Unable to write file'); - } - - $size = filesize($zipFile); - - if ($size == 0) { - //exit('Zero file'); - return self::response('Error creating archive'); - } - - header('Content-disposition: attachment; filename='.$archive); - header('Content-type: application/zip'); - readfile($zipFile); - //unlink($zipFile); - exit(); - - return self::response([ - 'file' => $zipName, - 'docsCount' => count($docs), - 'size' => $size, - ]); } public static function response($data)