'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 (например, " → ") $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");