Files
crm.clientright.ru/crm_extensions/file_storage/migrate_project_files.php
Fedor 3fb2ad5f60 feat: Project file migration and Nextcloud integration
- Added project file migration script with sanitization (underscores)
- Fixed Nextcloud editor integration (urldecode, basename fix)
- Added 'Open Project Folder in Nextcloud' button
- 223 projects migrated (completed + archived)
- URL decoding fix for Cyrillic filenames
2025-10-22 18:29:02 +03:00

357 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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");