✨ Features: - Migrated ALL files to new S3 structure (Projects, Contacts, Accounts, HelpDesk, Invoice, etc.) - Added Nextcloud folder buttons to ALL modules - Fixed Nextcloud editor integration - WebSocket server for real-time updates - Redis Pub/Sub integration - File path manager for organized storage - Redis caching for performance (Functions.php) 📁 New Structure: Documents/Project/ProjectName_ID/file_docID.ext Documents/Contacts/FirstName_LastName_ID/file_docID.ext Documents/Accounts/AccountName_ID/file_docID.ext 🔧 Technical: - FilePathManager for standardized paths - S3StorageService integration - WebSocket server (Node.js + Docker) - Redis cache for getBasicModuleInfo() - Predis library for Redis connectivity 📝 Scripts: - Migration scripts for all modules - Test pages for WebSocket/SSE/Polling - Documentation (MIGRATION_*.md, REDIS_*.md) 🎯 Result: 15,000+ files migrated successfully!
173 lines
5.9 KiB
PHP
173 lines
5.9 KiB
PHP
<?php
|
||
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:']);
|
||
$dryRun = isset($options['dry-run']);
|
||
$projectId = isset($options['project']) ? (int)$options['project'] : null;
|
||
|
||
if (!$projectId) {
|
||
die("❌ Укажите --project=ID\n");
|
||
}
|
||
|
||
$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('Ymd_His') . '.log';
|
||
|
||
if (!is_dir(__DIR__ . '/logs')) {
|
||
mkdir(__DIR__ . '/logs', 0755, true);
|
||
}
|
||
|
||
function writeLog($msg) {
|
||
global $logFile;
|
||
$line = "[" . date('Y-m-d H:i:s') . "] $msg\n";
|
||
file_put_contents($logFile, $line, FILE_APPEND);
|
||
echo $msg . "\n";
|
||
}
|
||
|
||
function sanitizeFolderName($name) {
|
||
// Убираем проблемные символы для папки
|
||
$name = str_replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|', '#'], '_', $name);
|
||
// Множественные пробелы → один пробел
|
||
$name = preg_replace('/\s+/', ' ', $name);
|
||
// Заменяем пробелы на подчёркивания
|
||
$name = str_replace(' ', '_', $name);
|
||
return trim($name);
|
||
}
|
||
|
||
function sanitizeFileName($name) {
|
||
$name = str_replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], '_', $name);
|
||
$name = preg_replace('/\s+/', ' ', $name);
|
||
return trim($name);
|
||
}
|
||
|
||
writeLog("🚀 === МИГРАЦИЯ ПРОЕКТА $projectId ===");
|
||
if ($dryRun) writeLog("⚠️ DRY-RUN MODE - НЕТ ИЗМЕНЕНИЙ");
|
||
|
||
// Получаем название проекта
|
||
$sql = "SELECT projectname FROM vtiger_project WHERE projectid = ?";
|
||
$result = $adb->pquery($sql, [$projectId]);
|
||
if ($adb->num_rows($result) === 0) {
|
||
die("❌ Проект $projectId не найден!\n");
|
||
}
|
||
|
||
$projectRow = $adb->fetchByAssoc($result);
|
||
$projectName = sanitizeFolderName($projectRow['projectname']);
|
||
|
||
writeLog("📋 Название проекта: {$projectRow['projectname']}");
|
||
writeLog("📁 Папка: {$projectName}_{$projectId}");
|
||
|
||
// Получаем документы проекта
|
||
$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\n");
|
||
|
||
$newFolderPath = "crm2/CRM_Active_Files/Documents/{$projectName}_{$projectId}";
|
||
$stats = ['total' => $count, 'success' => 0, 'errors' => 0, 'skipped' => 0];
|
||
$usedNames = [];
|
||
|
||
for ($i = 0; $i < $count; $i++) {
|
||
$doc = $adb->fetchByAssoc($result);
|
||
$docId = $doc['notesid'];
|
||
$title = sanitizeFileName($doc['title']);
|
||
$oldUrl = $doc['filename'];
|
||
|
||
writeLog("📄 [$docId] {$doc['title']}");
|
||
|
||
// Извлекаем S3 путь
|
||
if (strpos($oldUrl, "https://s3.twcstorage.ru/$bucket/") === 0) {
|
||
$oldS3PathEncoded = str_replace("https://s3.twcstorage.ru/$bucket/", '', $oldUrl);
|
||
$oldS3Path = urldecode($oldS3PathEncoded);
|
||
} else {
|
||
writeLog(" ⚠️ Нестандартный URL, пропускаю");
|
||
$stats['skipped']++;
|
||
continue;
|
||
}
|
||
|
||
// Формируем новое имя файла
|
||
$extension = pathinfo(basename($oldS3Path), PATHINFO_EXTENSION);
|
||
$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(" БЫЛО: $oldS3Path");
|
||
writeLog(" БУДЕТ: $newS3Path");
|
||
|
||
if ($dryRun) {
|
||
writeLog(" [DRY-RUN] ✓ Будет скопировано");
|
||
$stats['success']++;
|
||
continue;
|
||
}
|
||
|
||
// РЕАЛЬНОЕ КОПИРОВАНИЕ
|
||
try {
|
||
// Проверяем старый файл
|
||
$head = $s3->headObject(['Bucket' => $bucket, 'Key' => $oldS3Path]);
|
||
$oldSize = $head['ContentLength'];
|
||
writeLog(" ✓ Размер: " . number_format($oldSize / 1024, 2) . " KB");
|
||
|
||
// Копируем
|
||
$s3->copyObject([
|
||
'Bucket' => $bucket,
|
||
'CopySource' => "$bucket/$oldS3Path",
|
||
'Key' => $newS3Path,
|
||
]);
|
||
|
||
// Проверяем копию
|
||
$headNew = $s3->headObject(['Bucket' => $bucket, 'Key' => $newS3Path]);
|
||
if ($headNew['ContentLength'] !== $oldSize) {
|
||
throw new Exception("Размеры не совпадают!");
|
||
}
|
||
|
||
writeLog(" ✅ Скопировано");
|
||
|
||
// Обновляем БД
|
||
$newUrl = "https://s3.twcstorage.ru/$bucket/$newS3Path";
|
||
$adb->pquery("UPDATE vtiger_notes SET filename = ? WHERE notesid = ?", [$newUrl, $docId]);
|
||
|
||
writeLog(" ✅ БД обновлена");
|
||
$stats['success']++;
|
||
|
||
} catch (Exception $e) {
|
||
writeLog(" ❌ ОШИБКА: " . $e->getMessage());
|
||
$stats['errors']++;
|
||
}
|
||
}
|
||
|
||
writeLog("\n📊 === ИТОГО ===");
|
||
writeLog("Успешно: {$stats['success']} / {$stats['total']}");
|
||
writeLog("Ошибок: {$stats['errors']}");
|
||
writeLog("Пропущено: {$stats['skipped']}");
|
||
writeLog("✅ Лог: $logFile");
|