'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) { $name = str_replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], '_', $name); $name = preg_replace('/\s+/', ' ', $name); return trim($name); } function extractExtension($fileName) { $parts = explode('.', basename($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; } $newFolderPath = "crm2/CRM_Active_Files/Documents/проекта_{$projectId}"; writeLog("📁 Новая папка: $newFolderPath"); $stats = [ 'total' => $count, 'success' => 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']}"); // Извлекаем путь из URL и ДЕКОДИРУЕМ $oldS3Path = null; if (strpos($oldFileName, 'https://s3.twcstorage.ru/') === 0) { $oldS3Path = str_replace("https://s3.twcstorage.ru/$bucket/", '', $oldFileName); // ВАЖНО: Декодируем URL-encoded символы $oldS3Path = urldecode($oldS3Path); } elseif (strpos($oldFileName, 'crm2/') === 0) { $oldS3Path = urldecode($oldFileName); } if (!$oldS3Path) { writeLog(" ❌ Не удалось определить старый путь S3"); $stats['errors']++; continue; } writeLog(" Старый S3 путь: $oldS3Path"); $extension = extractExtension($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(" Новый S3 путь: $newS3Path"); if ($dryRun) { writeLog(" [DRY-RUN] ✓ Будет скопировано"); $stats['success']++; continue; } // РЕАЛЬНАЯ МИГРАЦИЯ try { // Проверяем старый файл $headObject = $s3->headObject([ 'Bucket' => $bucket, 'Key' => $oldS3Path, ]); $oldSize = $headObject['ContentLength']; writeLog(" ✓ Старый файл найден, размер: " . number_format($oldSize / 1024, 2) . " KB"); // Копируем 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("\n📊 === СТАТИСТИКА МИГРАЦИИ ==="); writeLog("Всего документов: {$stats['total']}"); writeLog("Успешно: {$stats['success']}"); writeLog("Ошибок: {$stats['errors']}"); return $stats; } writeLog("🚀 === СТАРТ МИГРАЦИИ ФАЙЛОВ (v2) ==="); writeLog("Время: " . date('Y-m-d H:i:s')); 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 > $backupFile 2>&1"; exec($backupCmd, $output, $returnCode); if (file_exists($backupFile) && filesize($backupFile) > 0) { writeLog("✅ Резервная копия создана: $backupFile"); } else { writeLog("❌ ОШИБКА создания резервной копии!"); writeLog("🛑 МИГРАЦИЯ ОТМЕНЕНА!"); exit(1); } } if ($projectId) { writeLog("\n🎯 Миграция проекта: $projectId"); migrateProject($projectId, $dryRun); } else { writeLog("\n❌ Укажите --project=ID"); exit(1); } writeLog("\n✅ === МИГРАЦИЯ ЗАВЕРШЕНА ==="); writeLog("Лог: $logFile");