Files
crm.clientright.ru/crm_extensions/file_storage/migrate_project_files.php

357 lines
14 KiB
PHP
Raw Normal View History

<?php
/**
* БЕЗОПАСНАЯ МИГРАЦИЯ ФАЙЛОВ ПРОЕКТА В НОВУЮ СТРУКТУРУ
*
* Старая структура: Documents/{documentId}/{fileName}
* Новая структура: Documents/проекта_{projectId}/{title}_{documentId}.ext
*
* БЕЗОПАСНОСТЬ:
* - Только КОПИРОВАНИЕ (НЕ удаление)
* - Проверка целостности (размер, существование)
* - Откат при ошибках
* - Детальное логирование
*/
chdir('/var/www/fastuser/data/www/crm.clientright.ru');
require_once 'include/utils/utils.php';
require_once 'include/database/PearDatabase.php';
require_once 'crm_extensions/vendor/autoload.php';
use Aws\S3\S3Client as AwsS3Client;
global $adb;
// Парсим аргументы командной строки
$options = getopt('', ['dry-run', 'project:', 'batch:', 'all', 'stats']);
$dryRun = isset($options['dry-run']);
$projectId = isset($options['project']) ? (int)$options['project'] : null;
$batchSize = isset($options['batch']) ? (int)$options['batch'] : 0;
$migrateAll = isset($options['all']);
$showStats = isset($options['stats']);
// Создаём S3 клиент
$s3 = new AwsS3Client([
'version' => 'latest',
'region' => 'ru-1',
'endpoint' => 'https://s3.twcstorage.ru',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => '2OMAK5ZNM900TAXM16J7',
'secret' => 'f4ADllb5VZBAt2HdsyB8WcwVEU7U74MwFCa1DARG',
],
]);
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
// Лог файл
$logFile = __DIR__ . '/logs/migration_' . date('Y-m-d_H-i-s') . '.log';
if (!is_dir(__DIR__ . '/logs')) {
mkdir(__DIR__ . '/logs', 0755, true);
}
function writeLog($message, $toScreen = true) {
global $logFile;
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[$timestamp] $message\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
if ($toScreen) {
echo $message . "\n";
}
}
function sanitizeFileName($name) {
// Декодируем HTML entities (например, &quot; → ")
$name = html_entity_decode($name, ENT_QUOTES, 'UTF-8');
// Убираем проблемные символы (включая кавычки и пробелы)
$name = str_replace(["/", "\\", ":", "*", "?", "\"", "<", ">", "|"], '_', $name);
// Заменяем все пробелы на подчёркивания
$name = preg_replace('/\s+/', '_', $name);
return trim($name);
}
function extractExtension($fileName) {
$parts = explode('.', $fileName);
return count($parts) > 1 ? array_pop($parts) : '';
}
function migrateProject($projectId, $dryRun = false) {
global $adb, $s3, $bucket;
writeLog("🔍 === МИГРАЦИЯ ПРОЕКТА $projectId ===");
if ($dryRun) {
writeLog("⚠️ РЕЖИМ DRY-RUN - изменения НЕ будут применены");
}
// Получаем все документы проекта
$sql = "SELECT n.* FROM vtiger_notes n
INNER JOIN vtiger_senotesrel r ON r.notesid = n.notesid
WHERE r.crmid = ? AND n.filelocationtype = 'E'
ORDER BY n.notesid";
$result = $adb->pquery($sql, [$projectId]);
$count = $adb->num_rows($result);
writeLog("📋 Найдено документов: $count");
if ($count === 0) {
writeLog("⚠️ Нет документов для миграции");
return;
}
// Получаем имя проекта для папки
$projectQuery = $adb->pquery("SELECT projectname FROM vtiger_project WHERE projectid = ?", [$projectId]);
if ($adb->num_rows($projectQuery) > 0) {
$projectName = $adb->query_result($projectQuery, 0, 'projectname');
$sanitizedProjectName = sanitizeFileName($projectName);
$newFolderPath = "crm2/CRM_Active_Files/Documents/{$sanitizedProjectName}_{$projectId}";
} else {
$newFolderPath = "crm2/CRM_Active_Files/Documents/project_{$projectId}";
}
writeLog("📁 Новая папка: $newFolderPath");
$stats = [
'total' => $count,
'success' => 0,
'skipped' => 0,
'errors' => 0,
];
$usedNames = []; // Для отслеживания дубликатов
for ($i = 0; $i < $count; $i++) {
$doc = $adb->fetchByAssoc($result);
$docId = $doc['notesid'];
$title = sanitizeFileName($doc['title']);
$oldFileName = $doc['filename'];
writeLog("\n📄 Документ $docId: {$doc['title']}");
writeLog(" Старый путь: $oldFileName");
// Извлекаем расширение из старого имени файла
$extension = extractExtension(basename($oldFileName));
// Формируем новое имя файла
$baseNewName = $title ? "{$title}_{$docId}" : "document_{$docId}";
$newFileName = $baseNewName . ($extension ? ".$extension" : '');
// Проверяем дубликаты
$counter = 1;
$finalNewName = $newFileName;
while (isset($usedNames[$finalNewName])) {
$finalNewName = $baseNewName . "_{$counter}" . ($extension ? ".$extension" : '');
$counter++;
}
$usedNames[$finalNewName] = true;
$newS3Path = "$newFolderPath/$finalNewName";
writeLog(" Новый путь: $newS3Path");
// Извлекаем старый S3 путь
$oldS3Path = null;
if (strpos($oldFileName, 'https://s3.twcstorage.ru/') === 0) {
// Полный URL - декодируем
$oldS3Path = str_replace("https://s3.twcstorage.ru/$bucket/", '', $oldFileName);
$oldS3Path = urldecode($oldS3Path); // Декодируем URL-кодированные символы
} elseif (strpos($oldFileName, 'crm2/') === 0) {
// Уже путь
$oldS3Path = urldecode($oldFileName); // Декодируем на всякий случай
}
if (!$oldS3Path) {
writeLog("Не удалось определить старый путь S3");
$stats['errors']++;
continue;
}
writeLog(" Старый S3: $oldS3Path");
if ($dryRun) {
writeLog(" [DRY-RUN] Будет скопировано: $oldS3Path$newS3Path");
$stats['success']++;
continue;
}
// РЕАЛЬНАЯ МИГРАЦИЯ
try {
// Проверяем что старый файл существует
try {
$headObject = $s3->headObject([
'Bucket' => $bucket,
'Key' => $oldS3Path,
]);
$oldSize = $headObject['ContentLength'];
writeLog(" ✓ Старый файл существует, размер: " . number_format($oldSize / 1024, 2) . " KB");
} catch (Exception $e) {
writeLog(" ❌ Старый файл не найден в S3: " . $e->getMessage());
$stats['errors']++;
continue;
}
// Копируем файл в новое место
writeLog(" 📋 Копирую файл...");
$s3->copyObject([
'Bucket' => $bucket,
'CopySource' => "$bucket/$oldS3Path",
'Key' => $newS3Path,
]);
// Проверяем что копия успешна
$headNewObject = $s3->headObject([
'Bucket' => $bucket,
'Key' => $newS3Path,
]);
$newSize = $headNewObject['ContentLength'];
if ($newSize !== $oldSize) {
throw new Exception("Размер не совпадает! Старый: $oldSize, Новый: $newSize");
}
writeLog(" ✅ Файл успешно скопирован, размер совпадает: " . number_format($newSize / 1024, 2) . " KB");
// Обновляем путь в базе данных
$newUrl = "https://s3.twcstorage.ru/$bucket/$newS3Path";
$updateSql = "UPDATE vtiger_notes SET filename = ? WHERE notesid = ?";
$adb->pquery($updateSql, [$newUrl, $docId]);
writeLog(" ✅ База данных обновлена");
writeLog(" ✅ УСПЕХ! Документ $docId мигрирован");
$stats['success']++;
} catch (Exception $e) {
writeLog(" ❌ ОШИБКА при миграции: " . $e->getMessage());
$stats['errors']++;
// Пытаемся удалить частично скопированный файл
try {
$s3->deleteObject([
'Bucket' => $bucket,
'Key' => $newS3Path,
]);
writeLog(" 🗑️ Частичная копия удалена");
} catch (Exception $cleanupError) {
writeLog(" ⚠️ Не удалось удалить частичную копию");
}
}
}
// Итоговая статистика
writeLog("\n📊 === СТАТИСТИКА МИГРАЦИИ ===");
writeLog("Всего документов: {$stats['total']}");
writeLog("Успешно: {$stats['success']}");
writeLog("Ошибок: {$stats['errors']}");
writeLog("Пропущено: {$stats['skipped']}");
return $stats;
}
// === ГЛАВНАЯ ЛОГИКА ===
// Если запрошена статистика - показываем и выходим
if ($showStats) {
echo "📊 === СТАТИСТИКА ДОКУМЕНТОВ В CRM ===\n";
echo "═══════════════════════════════════════\n";
// Общая статистика
$totalDocs = $adb->query("SELECT COUNT(*) as cnt FROM vtiger_notes WHERE filestatus = 1");
$totalDocsCount = $adb->query_result($totalDocs, 0, 'cnt');
$totalProjects = $adb->query("SELECT COUNT(DISTINCT projectid) as cnt FROM vtiger_senotesrel WHERE projectid IS NOT NULL AND projectid != ''");
$totalProjectsCount = $adb->query_result($totalProjects, 0, 'cnt');
$docsWithProjects = $adb->query("
SELECT COUNT(DISTINCT n.notesid) as cnt
FROM vtiger_notes n
INNER JOIN vtiger_senotesrel sr ON n.notesid = sr.notesid
WHERE n.filestatus = 1 AND sr.projectid IS NOT NULL AND sr.projectid != ''
");
$docsWithProjectsCount = $adb->query_result($docsWithProjects, 0, 'cnt');
$docsWithoutProjects = $totalDocsCount - $docsWithProjectsCount;
echo "📄 Всего активных документов: $totalDocsCount\n";
echo "📁 Всего проектов с документами: $totalProjectsCount\n";
echo "✅ Документов привязанных к проектам: $docsWithProjectsCount\n";
echo "⚠️ Документов БЕЗ проекта: $docsWithoutProjects\n\n";
// Топ-10 проектов
echo "🏆 ТОП-10 ПРОЕКТОВ ПО КОЛИЧЕСТВУ ДОКУМЕНТОВ:\n";
echo "═══════════════════════════════════════════════\n";
$topProjects = $adb->query("
SELECT
p.projectid,
p.projectname,
COUNT(n.notesid) as doc_count
FROM vtiger_project p
INNER JOIN vtiger_senotesrel sr ON p.projectid = sr.projectid
INNER JOIN vtiger_notes n ON sr.notesid = n.notesid
WHERE n.filestatus = 1
GROUP BY p.projectid, p.projectname
ORDER BY doc_count DESC
LIMIT 10
");
while ($row = $adb->fetch_array($topProjects)) {
$projectId = str_pad($row['projectid'], 6, ' ', STR_PAD_LEFT);
$projectName = mb_substr($row['projectname'], 0, 50);
$docCount = str_pad($row['doc_count'], 3, ' ', STR_PAD_LEFT);
echo " $projectId | $projectName | $docCount файлов\n";
}
exit(0);
}
writeLog("🚀 === СТАРТ МИГРАЦИИ ФАЙЛОВ ===");
writeLog("Время: " . date('Y-m-d H:i:s'));
writeLog("Лог файл: $logFile");
if ($dryRun) {
writeLog("\n⚠️⚠️⚠️ РЕЖИМ DRY-RUN - НИЧЕГО НЕ БУДЕТ ИЗМЕНЕНО ⚠️⚠️⚠️\n");
}
// Создаём бэкап базы данных ПЕРЕД миграцией
if (!$dryRun) {
writeLog("\n💾 === СОЗДАНИЕ РЕЗЕРВНОЙ КОПИИ БД ===");
$backupFile = "backup_before_migration_" . date('Y-m-d_H-i-s') . ".sql";
$backupCmd = "mysqldump -u ci20465_72new -p'EcY979Rn' ci20465_72new vtiger_notes vtiger_senotesrel vtiger_crmentity > $backupFile";
exec($backupCmd, $output, $returnCode);
if ($returnCode === 0) {
writeLog("✅ Резервная копия создана: $backupFile");
} else {
writeLog("❌ ОШИБКА создания резервной копии!");
writeLog("🛑 МИГРАЦИЯ ОТМЕНЕНА ДЛЯ БЕЗОПАСНОСТИ!");
exit(1);
}
}
// Выполняем миграцию
if ($projectId) {
// Один проект
writeLog("\n🎯 Миграция проекта: $projectId");
migrateProject($projectId, $dryRun);
} elseif ($batchSize > 0) {
// Пакет проектов
writeLog("\n📦 Миграция пакета проектов: $batchSize");
// TODO: реализовать позже
} elseif ($migrateAll) {
// Все проекты
writeLog("\n🌍 Миграция ВСЕХ проектов");
// TODO: реализовать позже
} else {
writeLog("\nНе указан режим миграции!");
writeLog("Использование:");
writeLog(" --dry-run --project=ID Тестовый прогон одного проекта");
writeLog(" --project=ID Миграция одного проекта");
writeLog(" --batch=100 Миграция пакета проектов");
writeLog(" --all Миграция всех проектов");
exit(1);
}
writeLog("\n✅ === МИГРАЦИЯ ЗАВЕРШЕНА ===");
writeLog("Лог файл: $logFile");