2025-09-26 10:43:05 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
class Vtiger_Base_Service
|
|
|
|
|
|
{
|
2025-11-21 10:23:52 +03:00
|
|
|
|
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 = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 10:43:05 +03:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-11-21 10:23:52 +03:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Получение документов из связанных сущностей (для проектов)
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
|
|
|
|
|
public static function getPaths($docs = [])
|
|
|
|
|
|
{
|
2025-11-21 10:23:52 +03:00
|
|
|
|
$archived = 0;
|
2025-09-26 10:43:05 +03:00
|
|
|
|
$errors = [];
|
|
|
|
|
|
$files = [];
|
2025-11-21 10:23:52 +03:00
|
|
|
|
|
|
|
|
|
|
// Отладочное логирование
|
|
|
|
|
|
error_log("========================================");
|
|
|
|
|
|
error_log("getPaths: Processing " . count($docs) . " documents");
|
|
|
|
|
|
|
2025-09-26 10:43:05 +03:00
|
|
|
|
foreach ($docs as $x) {
|
2025-11-21 10:23:52 +03:00
|
|
|
|
// Поддержка как 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;
|
2025-09-26 10:43:05 +03:00
|
|
|
|
}
|
2025-11-21 10:23:52 +03:00
|
|
|
|
|
|
|
|
|
|
$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'));
|
2025-09-26 10:43:05 +03:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2025-11-21 10:23:52 +03:00
|
|
|
|
|
|
|
|
|
|
// Проверяем условия для 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;
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
$archived++;
|
|
|
|
|
|
$files[] = [
|
|
|
|
|
|
'name' => $name,
|
|
|
|
|
|
'path' => $fullPath,
|
|
|
|
|
|
'is_temp' => false
|
|
|
|
|
|
];
|
|
|
|
|
|
error_log("getPaths: Added local file: {$name}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
$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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 10:43:05 +03:00
|
|
|
|
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) {
|
2025-11-21 10:23:52 +03:00
|
|
|
|
self::cleanupTempFiles();
|
2025-09-26 10:43:05 +03:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$ts = date('Ymd_His_') . array_pop(explode('.', microtime(1)));
|
|
|
|
|
|
$zipFile = "cache/{$id}_documents_{$ts}.zip";
|
|
|
|
|
|
$zip = new ZipArchive();
|
2025-11-21 10:23:52 +03:00
|
|
|
|
$result = $zip->open($zipFile, ZipArchive::CREATE);
|
|
|
|
|
|
if (!$result) {
|
|
|
|
|
|
self::cleanupTempFiles();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-26 10:43:05 +03:00
|
|
|
|
foreach ($files['files'] as $x) {
|
|
|
|
|
|
$zip->addFile($x['path'], $x['name']);
|
|
|
|
|
|
}
|
|
|
|
|
|
$zip->close();
|
|
|
|
|
|
|
|
|
|
|
|
$size = filesize($zipFile);
|
|
|
|
|
|
if ($size == 0) {
|
2025-11-21 10:23:52 +03:00
|
|
|
|
self::cleanupTempFiles();
|
2025-09-26 10:43:05 +03:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-11-21 10:23:52 +03:00
|
|
|
|
|
|
|
|
|
|
// Очищаем временные файлы после успешного создания архива
|
|
|
|
|
|
self::cleanupTempFiles();
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'total' => count($docs),
|
|
|
|
|
|
'archived' => $files['archived'],
|
|
|
|
|
|
'path' => $zipFile,
|
|
|
|
|
|
'size' => $size,
|
|
|
|
|
|
'errors' => $files['errors'],
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static function getArchive($id)
|
|
|
|
|
|
{
|
2025-11-21 10:23:52 +03:00
|
|
|
|
// Логирование через 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');
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
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)
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
$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');
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
foreach ($files['files'] as $x) {
|
|
|
|
|
|
$zip->addFile($x['path'], $x['name']);
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
$result = $zip->close();
|
|
|
|
|
|
if (! $result) {
|
|
|
|
|
|
self::cleanupTempFiles();
|
|
|
|
|
|
return self::response('Unable to write file');
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
$size = filesize($zipFile);
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
if ($size == 0) {
|
|
|
|
|
|
self::cleanupTempFiles();
|
|
|
|
|
|
return self::response('Error creating archive');
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
// Очищаем временные файлы после успешного создания архива
|
|
|
|
|
|
self::cleanupTempFiles();
|
2025-09-26 10:43:05 +03:00
|
|
|
|
|
2025-11-21 10:23:52 +03:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
2025-09-26 10:43:05 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static function response($data)
|
|
|
|
|
|
{
|
|
|
|
|
|
$response = new Vtiger_Response();
|
|
|
|
|
|
$response->setResult($data);
|
|
|
|
|
|
|
|
|
|
|
|
return $response->emit();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|