- Исправлен N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js: использовать uploads_field_labels[0] вместо [grp] - Создан SQL_CLAIMSAVE_FIXED_NEW_FLOW_DEDUP.sql с дедупликацией documents_meta - Создан SQL_CLEANUP_DOCUMENTS_META_DUPLICATES.sql для очистки существующих дубликатов - Создан полный уникальный индекс idx_document_texts_hash_unique на document_texts(file_hash) - Добавлен SESSION_LOG_2025-11-28_documents_dedup.md с описанием всех изменений Fixes: - field_label теперь корректно отображает 'Переписка' вместо 'group-2' - documents_meta не накапливает дубликаты при повторных сохранениях - ON CONFLICT (file_hash) теперь работает для document_texts
275 lines
14 KiB
PHP
Executable File
275 lines
14 KiB
PHP
Executable File
<?php
|
||
/**
|
||
* Скрипт для массового восстановления всех удаленных файлов из S3
|
||
*
|
||
* Восстанавливает файлы, удаленные через delete markers, удаляя эти маркеры
|
||
*/
|
||
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', 1);
|
||
set_time_limit(0); // Без ограничения времени
|
||
|
||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
|
||
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
|
||
|
||
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
|
||
$s3Bucket = $config['s3']['bucket'];
|
||
|
||
echo "Массовое восстановление удаленных файлов из S3\n";
|
||
echo str_repeat("=", 80) . "\n\n";
|
||
|
||
// Параметры
|
||
$dryRun = isset($argv[1]) && $argv[1] === '--dry-run';
|
||
$limit = isset($argv[2]) ? (int)$argv[2] : null; // Ограничение количества файлов
|
||
$prefix = isset($argv[3]) ? $argv[3] : 'crm2/CRM_Active_Files/Documents/'; // Префикс для поиска
|
||
|
||
if ($dryRun) {
|
||
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run) - файлы не будут восстановлены\n\n";
|
||
}
|
||
|
||
try {
|
||
$s3Client = new \Aws\S3\S3Client([
|
||
'version' => 'latest',
|
||
'region' => $config['s3']['region'],
|
||
'endpoint' => $config['s3']['endpoint'],
|
||
'use_path_style_endpoint' => true,
|
||
'credentials' => [
|
||
'key' => $config['s3']['key'],
|
||
'secret' => $config['s3']['secret'],
|
||
],
|
||
'suppress_php_deprecation_warning' => true
|
||
]);
|
||
|
||
$stats = [
|
||
'total_markers' => 0,
|
||
'restored' => 0,
|
||
'failed' => 0,
|
||
'skipped' => 0,
|
||
'errors' => []
|
||
];
|
||
|
||
$processedKeys = []; // Для отслеживания уже обработанных ключей
|
||
|
||
echo "Поиск delete markers в префиксе: $prefix\n";
|
||
echo "Ограничение: " . ($limit ? "$limit файлов" : "нет") . "\n\n";
|
||
|
||
$isTruncated = true;
|
||
$continuationToken = null;
|
||
$pageCount = 0;
|
||
$maxPages = isset($argv[4]) ? (int)$argv[4] : 10; // БЕЗОПАСНО: максимум 10 страниц по умолчанию (можно увеличить через параметр)
|
||
|
||
echo "⚠️ ВНИМАНИЕ: Обработка ограничена {$maxPages} страницами для безопасности\n";
|
||
echo " Для обработки большего количества используйте: php restore_all_deleted_files.php [--dry-run] [limit] [prefix] [maxPages]\n\n";
|
||
|
||
while ($isTruncated && $pageCount < $maxPages && (!$limit || $stats['restored'] + $stats['failed'] < $limit)) {
|
||
$params = [
|
||
'Bucket' => $s3Bucket,
|
||
'Prefix' => $prefix,
|
||
'MaxKeys' => 100 // БЕЗОПАСНО: уменьшено с 1000 до 100 для снижения нагрузки
|
||
];
|
||
|
||
if ($continuationToken) {
|
||
$params['ContinuationToken'] = $continuationToken;
|
||
}
|
||
|
||
echo "Обработка страницы " . ($pageCount + 1) . "/{$maxPages}...\r";
|
||
|
||
try {
|
||
$versions = $s3Client->listObjectVersions($params);
|
||
$pageCount++;
|
||
|
||
// БЕЗОПАСНОСТЬ: пауза между страницами для снижения нагрузки
|
||
if ($pageCount < $maxPages && $isTruncated) {
|
||
usleep(500000); // 0.5 секунды пауза между страницами
|
||
}
|
||
|
||
if (isset($versions['DeleteMarkers']) && !empty($versions['DeleteMarkers'])) {
|
||
foreach ($versions['DeleteMarkers'] as $marker) {
|
||
$key = $marker['Key'];
|
||
$versionId = $marker['VersionId'];
|
||
$deleteDate = $marker['LastModified'] ?? 'не указана';
|
||
|
||
// Пропускаем, если уже обработали этот ключ
|
||
if (isset($processedKeys[$key])) {
|
||
continue;
|
||
}
|
||
|
||
$stats['total_markers']++;
|
||
|
||
// Проверяем лимит
|
||
if ($limit && ($stats['restored'] + $stats['failed']) >= $limit) {
|
||
break 2; // Выходим из обоих циклов
|
||
}
|
||
|
||
// Пропускаем папки (заканчиваются на /)
|
||
if (substr($key, -1) === '/') {
|
||
$stats['skipped']++;
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
if (!$dryRun) {
|
||
// Сначала проверяем, есть ли версии файла
|
||
try {
|
||
$versionsList = $s3Client->listObjectVersions([
|
||
'Bucket' => $s3Bucket,
|
||
'Prefix' => $key,
|
||
'MaxKeys' => 10
|
||
]);
|
||
|
||
$hasVersions = isset($versionsList['Versions']) && !empty($versionsList['Versions']);
|
||
|
||
if ($hasVersions) {
|
||
// Есть версии - удаляем delete marker и файл восстановится автоматически
|
||
$s3Client->deleteObject([
|
||
'Bucket' => $s3Bucket,
|
||
'Key' => $key,
|
||
'VersionId' => $versionId
|
||
]);
|
||
|
||
// Проверяем, восстановился ли файл
|
||
try {
|
||
$headResult = $s3Client->headObject([
|
||
'Bucket' => $s3Bucket,
|
||
'Key' => $key
|
||
]);
|
||
|
||
$stats['restored']++;
|
||
$processedKeys[$key] = true;
|
||
|
||
// БЕЗОПАСНОСТЬ: пауза каждые 10 файлов
|
||
if ($stats['restored'] % 10 == 0) {
|
||
echo "Восстановлено: {$stats['restored']} файлов...\r";
|
||
usleep(200000); // 0.2 секунды пауза
|
||
}
|
||
|
||
} catch (\Aws\Exception\AwsException $e) {
|
||
if ($e->getAwsErrorCode() == 'NotFound') {
|
||
// Файл все еще не доступен, пробуем восстановить последнюю версию вручную
|
||
$latestVersion = $versionsList['Versions'][0];
|
||
$latestVersionId = $latestVersion['VersionId'];
|
||
|
||
try {
|
||
// Копируем версию в текущий объект
|
||
$s3Client->copyObject([
|
||
'Bucket' => $s3Bucket,
|
||
'CopySource' => urlencode($s3Bucket . '/' . $key) . '?versionId=' . $latestVersionId,
|
||
'Key' => $key
|
||
]);
|
||
|
||
$stats['restored']++;
|
||
$processedKeys[$key] = true;
|
||
|
||
if ($stats['restored'] % 100 == 0) {
|
||
echo "Восстановлено: {$stats['restored']} файлов...\r";
|
||
}
|
||
} catch (\Aws\Exception\AwsException $e2) {
|
||
$stats['failed']++;
|
||
$stats['errors'][] = "Ошибка восстановления версии $key: " . $e2->getMessage();
|
||
}
|
||
} else {
|
||
$stats['failed']++;
|
||
$stats['errors'][] = "Ошибка проверки $key: " . $e->getMessage();
|
||
}
|
||
}
|
||
} else {
|
||
// Нет версий - файл удален безвозвратно
|
||
$stats['failed']++;
|
||
$stats['errors'][] = "Не найдена версия для восстановления: $key (файл удален безвозвратно)";
|
||
}
|
||
|
||
} catch (\Aws\Exception\AwsException $e) {
|
||
$stats['failed']++;
|
||
$stats['errors'][] = "Ошибка проверки версий для $key: " . $e->getMessage();
|
||
}
|
||
} else {
|
||
// Dry-run режим - проверяем наличие версий
|
||
try {
|
||
$versionsList = $s3Client->listObjectVersions([
|
||
'Bucket' => $s3Bucket,
|
||
'Prefix' => $key,
|
||
'MaxKeys' => 1
|
||
]);
|
||
|
||
if (isset($versionsList['Versions']) && !empty($versionsList['Versions'])) {
|
||
$stats['restored']++;
|
||
} else {
|
||
$stats['failed']++;
|
||
}
|
||
$processedKeys[$key] = true;
|
||
|
||
if (($stats['restored'] + $stats['failed']) % 100 == 0) {
|
||
echo "Проверено: " . ($stats['restored'] + $stats['failed']) . " файлов...\r";
|
||
}
|
||
} catch (\Aws\Exception\AwsException $e) {
|
||
$stats['failed']++;
|
||
$processedKeys[$key] = true;
|
||
}
|
||
}
|
||
|
||
} catch (\Aws\Exception\AwsException $e) {
|
||
$stats['failed']++;
|
||
$stats['errors'][] = "Ошибка удаления delete marker для $key: " . $e->getMessage();
|
||
|
||
if (count($stats['errors']) <= 10) {
|
||
echo "\n Ошибка: {$stats['errors'][count($stats['errors']) - 1]}\n";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
$isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated'];
|
||
$continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null;
|
||
|
||
if (!$isTruncated) {
|
||
break;
|
||
}
|
||
|
||
} catch (\Aws\Exception\AwsException $e) {
|
||
echo "\n❌ Ошибка при обработке страницы: " . $e->getMessage() . "\n";
|
||
break;
|
||
}
|
||
}
|
||
|
||
echo "\n\n";
|
||
echo str_repeat("=", 80) . "\n";
|
||
echo "ИТОГОВЫЙ ОТЧЕТ:\n\n";
|
||
|
||
echo "Всего найдено delete markers: {$stats['total_markers']}\n";
|
||
echo "Восстановлено файлов: {$stats['restored']}\n";
|
||
echo "Ошибок: {$stats['failed']}\n";
|
||
echo "Пропущено (папки): {$stats['skipped']}\n\n";
|
||
|
||
if (!empty($stats['errors']) && count($stats['errors']) <= 20) {
|
||
echo "Ошибки (первые " . count($stats['errors']) . "):\n";
|
||
foreach ($stats['errors'] as $error) {
|
||
echo " - $error\n";
|
||
}
|
||
echo "\n";
|
||
} elseif (!empty($stats['errors'])) {
|
||
echo "Всего ошибок: " . count($stats['errors']) . " (показаны первые 20)\n";
|
||
foreach (array_slice($stats['errors'], 0, 20) as $error) {
|
||
echo " - $error\n";
|
||
}
|
||
echo "\n";
|
||
}
|
||
|
||
if ($dryRun) {
|
||
echo "⚠️ Это был режим проверки. Для реального восстановления запустите:\n";
|
||
echo " php restore_all_deleted_files.php\n\n";
|
||
} else {
|
||
echo "✅ Восстановление завершено!\n";
|
||
echo " Проверьте доступность файлов в интерфейсе CRM\n\n";
|
||
}
|
||
|
||
// Сохраняем статистику в файл
|
||
$logFile = '/var/www/fastuser/data/www/crm.clientright.ru/restore_log_' . date('Y-m-d_H-i-s') . '.json';
|
||
file_put_contents($logFile, json_encode($stats, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
echo "📝 Лог сохранен в: $logFile\n";
|
||
|
||
} catch (Exception $e) {
|
||
echo "❌ КРИТИЧЕСКАЯ ОШИБКА: " . $e->getMessage() . "\n";
|
||
echo "Trace: " . $e->getTraceAsString() . "\n";
|
||
}
|
||
|