- Добавлен метод getRelatedDocs() для получения документов из связанных сущностей (контакты, контрагенты) - Добавлен метод downloadS3File() для скачивания файлов из S3 во временную папку - Добавлен метод cleanupTempFiles() для очистки временных файлов - Исправлен getPaths() для корректной обработки S3 файлов (всегда запрашивает s3_bucket/s3_key из БД) - Исправлен getArchive() для проектов: собирает документы из основной записи и связанных сущностей - Исправлен путь к vendor/autoload.php (поиск по нескольким путям) - Исправлено имя временного файла (короткое имя вместо полного пути для избежания 'File name too long') Результат: архив успешно создается с документами из проекта и связанных сущностей (25 документов для проекта 396447)
655 lines
30 KiB
PHP
655 lines
30 KiB
PHP
<?php
|
||
|
||
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';
|
||
$relation = Vtiger_RelationListView_Model::getInstance(
|
||
$record,
|
||
$module
|
||
);
|
||
|
||
$pager = new Vtiger_Paging_Model();
|
||
$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;
|
||
$errors = [];
|
||
$files = [];
|
||
|
||
// Отладочное логирование
|
||
error_log("========================================");
|
||
error_log("getPaths: Processing " . count($docs) . " documents");
|
||
|
||
foreach ($docs as $x) {
|
||
// Поддержка как 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;
|
||
}
|
||
|
||
$archived++;
|
||
$files[] = [
|
||
'name' => $name,
|
||
'path' => $fullPath,
|
||
'is_temp' => false
|
||
];
|
||
error_log("getPaths: Added local file: {$name}");
|
||
}
|
||
}
|
||
|
||
$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',
|
||
'archived'
|
||
);
|
||
}
|
||
|
||
public static function createArchive($id)
|
||
{
|
||
$record = Vtiger_Record_Model::getInstanceById($id);
|
||
if (! $record) {
|
||
return false;
|
||
}
|
||
|
||
$docs = self::getDocs($record);
|
||
if (count($docs) == 0) {
|
||
return false;
|
||
}
|
||
|
||
$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();
|
||
$result = $zip->open($zipFile, ZipArchive::CREATE);
|
||
if (!$result) {
|
||
self::cleanupTempFiles();
|
||
return false;
|
||
}
|
||
|
||
foreach ($files['files'] as $x) {
|
||
$zip->addFile($x['path'], $x['name']);
|
||
}
|
||
$zip->close();
|
||
|
||
$size = filesize($zipFile);
|
||
if ($size == 0) {
|
||
self::cleanupTempFiles();
|
||
return false;
|
||
}
|
||
|
||
// Очищаем временные файлы после успешного создания архива
|
||
self::cleanupTempFiles();
|
||
|
||
return [
|
||
'total' => count($docs),
|
||
'archived' => $files['archived'],
|
||
'path' => $zipFile,
|
||
'size' => $size,
|
||
'errors' => $files['errors'],
|
||
];
|
||
}
|
||
|
||
public static function getArchive($id)
|
||
{
|
||
// Логирование через 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());
|
||
}
|
||
}
|
||
|
||
public static function response($data)
|
||
{
|
||
$response = new Vtiger_Response();
|
||
$response->setResult($data);
|
||
|
||
return $response->emit();
|
||
}
|
||
}
|