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
This commit is contained in:
@@ -9,7 +9,15 @@ $recordId = isset($_GET['recordId']) ? $_GET['recordId'] : '';
|
|||||||
|
|
||||||
// Если fileName содержит полный URL, извлекаем только имя файла
|
// Если fileName содержит полный URL, извлекаем только имя файла
|
||||||
if (strpos($fileName, 'http') === 0) {
|
if (strpos($fileName, 'http') === 0) {
|
||||||
$fileName = basename($fileName);
|
$fileName = urldecode($fileName);
|
||||||
|
// ИСПРАВЛЕНИЕ: используем правильное извлечение имени файла
|
||||||
|
$lastSlash = strrpos($fileName, '/');
|
||||||
|
if ($lastSlash !== false) {
|
||||||
|
$fileName = substr($fileName, $lastSlash + 1);
|
||||||
|
} else {
|
||||||
|
$fileName = basename($fileName);
|
||||||
|
}
|
||||||
|
$fileName = trim($fileName);
|
||||||
error_log("Nextcloud Editor: Извлечено имя файла из URL: {$fileName}");
|
error_log("Nextcloud Editor: Извлечено имя файла из URL: {$fileName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,59 +26,128 @@ $nextcloudUrl = 'https://office.clientright.ru:8443';
|
|||||||
$username = 'admin';
|
$username = 'admin';
|
||||||
$password = 'office';
|
$password = 'office';
|
||||||
|
|
||||||
// Путь к файлу в Nextcloud (относительно папки пользователя admin)
|
// Подключаемся к БД чтобы получить название проекта
|
||||||
// Предполагаем, что файлы хранятся по пути: crm/crm2/CRM_Active_Files/Documents/{recordId}/{fileName}
|
chdir('/var/www/fastuser/data/www/crm.clientright.ru');
|
||||||
$nextcloudFilePath = "crm/crm2/CRM_Active_Files/Documents/{$recordId}/" . urlencode($fileName);
|
require_once 'include/utils/utils.php';
|
||||||
|
require_once 'include/database/PearDatabase.php';
|
||||||
|
|
||||||
|
global $adb;
|
||||||
|
|
||||||
|
// Функция для санитизации названия папки
|
||||||
|
function sanitizeFolderName($name) {
|
||||||
|
$name = str_replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|', '#'], '_', $name);
|
||||||
|
$name = preg_replace('/\s+/', '_', $name);
|
||||||
|
return trim($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Найдём projectid по связи документа → проекта
|
||||||
|
$docId = $recordId;
|
||||||
|
$projectId = null;
|
||||||
|
try {
|
||||||
|
$sqlProject = "SELECT r.crmid AS projectid
|
||||||
|
FROM vtiger_senotesrel r
|
||||||
|
INNER JOIN vtiger_crmentity e ON e.crmid = r.crmid
|
||||||
|
WHERE r.notesid = ? AND e.setype = 'Project'
|
||||||
|
ORDER BY r.crmid DESC LIMIT 1";
|
||||||
|
$resProject = $adb->pquery($sqlProject, [$docId]);
|
||||||
|
if ($resProject && $adb->num_rows($resProject) > 0) {
|
||||||
|
$projectRow = $adb->fetchByAssoc($resProject);
|
||||||
|
$projectId = (string)$projectRow['projectid'];
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Nextcloud Editor: DB error while resolving project by document: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем название проекта из БД (если нашли projectId)
|
||||||
|
$projectName = null;
|
||||||
|
if ($projectId) {
|
||||||
|
$sql = "SELECT projectname FROM vtiger_project WHERE projectid = ?";
|
||||||
|
$result = $adb->pquery($sql, [$projectId]);
|
||||||
|
if ($result && $adb->num_rows($result) > 0) {
|
||||||
|
$row = $adb->fetchByAssoc($result);
|
||||||
|
$projectName = sanitizeFolderName($row['projectname']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем пути к файлу в Nextcloud
|
||||||
|
// НОВЫЙ формат: crm/crm2/CRM_Active_Files/Documents/{ProjectName}_{ProjectID}/{fileName}
|
||||||
|
// СТАРЫЙ формат: crm/crm2/CRM_Active_Files/Documents/{DocumentID}/{fileName}
|
||||||
|
|
||||||
|
// Вспомогательная функция: кодирование пути по сегментам (WebDAV)
|
||||||
|
$encodePath = function(array $segments) {
|
||||||
|
return implode('/', array_map('rawurlencode', $segments));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Список путей для логирования (читаемые) и для запроса (url-encoded)
|
||||||
|
$humanPaths = [];
|
||||||
|
$requestPaths = [];
|
||||||
|
|
||||||
|
if ($projectName && $projectId) {
|
||||||
|
$humanPaths[] = "crm/crm2/CRM_Active_Files/Documents/{$projectName}_{$projectId}/{$fileName}";
|
||||||
|
$requestPaths[] = $encodePath(['crm','crm2','CRM_Active_Files','Documents',"{$projectName}_{$projectId}",$fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Резерв - старый формат (папка по documentId)
|
||||||
|
$humanPaths[] = "crm/crm2/CRM_Active_Files/Documents/{$docId}/{$fileName}";
|
||||||
|
$requestPaths[] = $encodePath(['crm','crm2','CRM_Active_Files','Documents',(string)$docId,$fileName]);
|
||||||
|
|
||||||
$fileId = null;
|
$fileId = null;
|
||||||
|
$usedPath = null;
|
||||||
|
|
||||||
// Попытка получить fileId через WebDAV PROPFIND
|
// Пробуем найти файл по всем возможным путям
|
||||||
error_log("Nextcloud Editor: Попытка получить fileId для файла: {$nextcloudFilePath} через WebDAV");
|
for ($i = 0; $i < count($requestPaths); $i++) {
|
||||||
|
$tryPath = $requestPaths[$i];
|
||||||
// XML запрос для получения fileid
|
$logPath = $humanPaths[$i];
|
||||||
$xmlRequest = '<?xml version="1.0"?>
|
$propfindUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . '/' . $tryPath;
|
||||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
// error_log("Nextcloud Editor: PROPFIND -> {$propfindUrl} (читаемый путь: {$logPath})");
|
||||||
<d:prop>
|
|
||||||
<oc:fileid/>
|
// XML запрос для получения fileid
|
||||||
</d:prop>
|
$xmlRequest = '<?xml version="1.0"?>
|
||||||
</d:propfind>';
|
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||||
|
<d:prop>
|
||||||
$ch = curl_init();
|
<oc:fileid/>
|
||||||
curl_setopt($ch, CURLOPT_URL, $nextcloudUrl . '/remote.php/dav/files/' . $username . '/' . $nextcloudFilePath);
|
</d:prop>
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
</d:propfind>';
|
||||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ':' . $password);
|
|
||||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
|
$ch = curl_init();
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $xmlRequest);
|
curl_setopt($ch, CURLOPT_URL, $propfindUrl);
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
'Depth: 0',
|
curl_setopt($ch, CURLOPT_USERPWD, $username . ':' . $password);
|
||||||
'Content-Type: application/xml'
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
|
||||||
]);
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $xmlRequest);
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
$response = curl_exec($ch);
|
'Depth: 0',
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
'Content-Type: application/xml'
|
||||||
$curlError = curl_error($ch);
|
]);
|
||||||
curl_close($ch);
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
$response = curl_exec($ch);
|
||||||
if ($response === false) {
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
error_log("Nextcloud Editor: Ошибка cURL при запросе WebDAV: " . $curlError);
|
$curlError = curl_error($ch);
|
||||||
} else {
|
curl_close($ch);
|
||||||
error_log("Nextcloud Editor: WebDAV Response (HTTP {$httpCode}): " . substr($response, 0, 500));
|
|
||||||
|
if ($response === false) {
|
||||||
|
error_log("Nextcloud Editor: Ошибка cURL: " . $curlError);
|
||||||
|
continue; // Пробуем следующий путь
|
||||||
|
}
|
||||||
|
|
||||||
if ($httpCode === 207 && $response) { // 207 = Multi-Status для PROPFIND
|
if ($httpCode === 207 && $response) { // 207 = Multi-Status для PROPFIND
|
||||||
// Простой regex для извлечения fileid
|
// Простой regex для извлечения fileid
|
||||||
if (preg_match('/<oc:fileid>(\d+)<\/oc:fileid>/', $response, $matches)) {
|
if (preg_match('/<oc:fileid>(\d+)<\/oc:fileid>/', $response, $matches)) {
|
||||||
$fileId = $matches[1];
|
$fileId = $matches[1];
|
||||||
error_log("Nextcloud Editor: fileId получен через WebDAV regex: " . $fileId);
|
$usedPath = $tryPath;
|
||||||
} else {
|
error_log("Nextcloud Editor: ✅ fileId получен: {$fileId} (путь: {$usedPath})");
|
||||||
error_log("Nextcloud Editor: fileid не найден в XML ответе");
|
break; // Нашли файл, выходим из цикла
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
error_log("Nextcloud Editor: WebDAV запрос неуспешен. HTTP Code: {$httpCode}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// error_log("Nextcloud Editor: Файл не найден по пути: {$logPath} (HTTP {$httpCode})");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$fileId) {
|
if (!$fileId) {
|
||||||
die('❌ Ошибка: Не удалось получить fileId для файла ' . $fileName);
|
// Простая ошибка без отладки
|
||||||
|
$errorMsg = "❌ Ошибка: Не удалось получить fileId для файла {$fileName}";
|
||||||
|
error_log("Nextcloud Editor ERROR: " . $errorMsg);
|
||||||
|
die($errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Формируем URL для Nextcloud
|
// Формируем URL для Nextcloud
|
||||||
|
|||||||
356
crm_extensions/file_storage/migrate_project_files.php
Normal file
356
crm_extensions/file_storage/migrate_project_files.php
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
<?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 (например, " → ")
|
||||||
|
$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");
|
||||||
@@ -3,32 +3,46 @@
|
|||||||
* JavaScript для интеграции редактора документов Nextcloud
|
* JavaScript для интеграции редактора документов Nextcloud
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открытие папки проекта в Nextcloud
|
||||||
|
*/
|
||||||
|
function openProjectFolder(projectId, projectName) {
|
||||||
|
// Нормализуем имя проекта (убираем множественные пробелы, как в sanitizeFileName)
|
||||||
|
if (projectName) {
|
||||||
|
projectName = projectName.replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем URL для папки проекта в Nextcloud
|
||||||
|
const folderName = projectName ? `${projectName}_${projectId}` : `project_${projectId}`;
|
||||||
|
const encodedFolderName = encodeURIComponent(folderName);
|
||||||
|
const nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
|
||||||
|
// URL для папки проекта в Nextcloud External Storage
|
||||||
|
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/${encodedFolderName}`;
|
||||||
|
|
||||||
|
// Открываем папку в новом окне
|
||||||
|
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обёртка для вызова из шаблонов (с подстановкой Smarty переменных)
|
||||||
|
*/
|
||||||
|
function openProjectFolderInNextcloud() {
|
||||||
|
// Эта функция будет вызываться из шаблона, где переменные уже подставлены
|
||||||
|
// См. DetailViewHeaderTitle.tpl - там прямой вызов с параметрами
|
||||||
|
console.warn('⚠️ openProjectFolderInNextcloud() called without parameters - use openProjectFolder(projectId, projectName) instead');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Открытие редактора Nextcloud для документа
|
* Открытие редактора Nextcloud для документа
|
||||||
*/
|
*/
|
||||||
function openNextcloudEditor(recordId, fileName) {
|
function openNextcloudEditor(recordId, fileName) {
|
||||||
console.log('🚀 NEXTCLOUD EDITOR: Function called!', recordId, fileName);
|
// ПРОСТОЕ РЕШЕНИЕ - используем промежуточную страницу для редиректа!
|
||||||
|
const cacheVersion = Date.now(); // Принудительное обновление кеша
|
||||||
// Сначала тестируем debug API
|
const redirectUrl = `/crm_extensions/file_storage/api/open_file.php?recordId=${recordId}&fileName=${encodeURIComponent(fileName)}&v=${cacheVersion}`;
|
||||||
console.log('🔍 Testing debug API...');
|
|
||||||
$.ajax({
|
// Открываем редактор в новом окне через промежуточную страницу
|
||||||
url: '/crm_extensions/file_storage/api/debug_api.php',
|
window.open(redirectUrl, 'nextcloud_editor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||||
method: 'GET',
|
|
||||||
dataType: 'json',
|
|
||||||
success: function(debugResponse) {
|
|
||||||
console.log('✅ Debug API works:', debugResponse);
|
|
||||||
|
|
||||||
// Теперь тестируем простой API
|
|
||||||
testSimpleAPI(recordId, fileName);
|
|
||||||
},
|
|
||||||
error: function(xhr, status, error) {
|
|
||||||
console.error('❌ Debug API failed:', error);
|
|
||||||
console.error('Response:', xhr.responseText);
|
|
||||||
|
|
||||||
// Все равно пытаемся вызвать основной API
|
|
||||||
callMainAPI(recordId, fileName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSimpleAPI(recordId, fileName) {
|
function testSimpleAPI(recordId, fileName) {
|
||||||
@@ -228,7 +242,7 @@ function openEditor(editUrl, fileData) {
|
|||||||
message: 'Редактор открыт! Файл: ' + fileData.file_name
|
message: 'Редактор открыт! Файл: ' + fileData.file_name
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
alert('✅ Редактор открыт! Файл: ' + fileData.file_name);
|
// alert('✅ Редактор открыт! Файл: ' + fileData.file_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем информацию о файле
|
// Показываем информацию о файле
|
||||||
@@ -477,4 +491,4 @@ $(document).ready(function() {
|
|||||||
`)
|
`)
|
||||||
.appendTo('head');
|
.appendTo('head');
|
||||||
}
|
}
|
||||||
});
|
});// Version: 1761125337
|
||||||
|
|||||||
@@ -3,6 +3,34 @@
|
|||||||
* JavaScript для интеграции редактора документов Nextcloud
|
* JavaScript для интеграции редактора документов Nextcloud
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открытие папки проекта в Nextcloud
|
||||||
|
*/
|
||||||
|
function openProjectFolder(projectId, projectName) {
|
||||||
|
console.log('📁 Opening project folder in Nextcloud:', projectId, projectName);
|
||||||
|
|
||||||
|
// Нормализуем имя проекта (убираем пробелы и кавычки)
|
||||||
|
if (projectName) {
|
||||||
|
// Убираем кавычки (заменяем на подчёркивание)
|
||||||
|
projectName = projectName.replace(/"/g, '_');
|
||||||
|
// Заменяем ВСЕ пробелы на подчёркивания
|
||||||
|
projectName = projectName.replace(/\s+/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем URL для папки проекта в Nextcloud
|
||||||
|
const folderName = projectName ? `${projectName}_${projectId}` : `project_${projectId}`;
|
||||||
|
const encodedFolderName = encodeURIComponent(folderName);
|
||||||
|
const nextcloudUrl = 'https://office.clientright.ru:8443';
|
||||||
|
|
||||||
|
// URL для папки проекта в Nextcloud External Storage
|
||||||
|
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/${encodedFolderName}`;
|
||||||
|
|
||||||
|
console.log('🔗 Folder URL:', folderUrl);
|
||||||
|
|
||||||
|
// Открываем папку в новом окне
|
||||||
|
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Открытие редактора Nextcloud для документа
|
* Открытие редактора Nextcloud для документа
|
||||||
*/
|
*/
|
||||||
@@ -19,7 +47,7 @@ function openNextcloudEditor(recordId, fileName) {
|
|||||||
|
|
||||||
if (win) {
|
if (win) {
|
||||||
console.log('✅ Editor opened successfully');
|
console.log('✅ Editor opened successfully');
|
||||||
alert('✅ Редактор открыт! Файл: ' + fileName);
|
// alert('✅ Редактор открыт! Файл: ' + fileName);
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ Failed to open editor window - popup blocked');
|
console.log('❌ Failed to open editor window - popup blocked');
|
||||||
alert('❌ Не удалось открыть редактор. Проверьте блокировку всплывающих окон.');
|
alert('❌ Не удалось открыть редактор. Проверьте блокировку всплывающих окон.');
|
||||||
@@ -190,7 +218,7 @@ function openEditor(editUrl, fileData) {
|
|||||||
message: 'Редактор открыт! Файл: ' + fileData.file_name
|
message: 'Редактор открыт! Файл: ' + fileData.file_name
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
alert('✅ Редактор открыт! Файл: ' + fileData.file_name);
|
// alert('✅ Редактор открыт! Файл: ' + fileData.file_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем информацию о файле
|
// Показываем информацию о файле
|
||||||
|
|||||||
@@ -1,56 +1,66 @@
|
|||||||
{*<!--
|
{*<!--
|
||||||
/*********************************************************************************
|
/*********************************************************************************
|
||||||
** The contents of this file are subject to the vtiger CRM Public License Version 1.0
|
** The contents of this file are subject to the vtiger CRM Public License Version 1.0
|
||||||
* ("License"); You may not use this file except in compliance with the License
|
* ("License"); You may not use this file except in compliance with the License
|
||||||
* The Original Code is: vtiger CRM Open Source
|
* The Original Code is: vtiger CRM Open Source
|
||||||
* The Initial Developer of the Original Code is vtiger.
|
* The Initial Developer of the Original Code is vtiger.
|
||||||
* Portions created by vtiger are Copyright (C) vtiger.
|
* Portions created by vtiger are Copyright (C) vtiger.
|
||||||
* All Rights Reserved.
|
* All Rights Reserved.
|
||||||
*
|
*
|
||||||
********************************************************************************/
|
********************************************************************************/
|
||||||
-->*}
|
-->*}
|
||||||
{strip}
|
{strip}
|
||||||
<div class="col-lg-6 col-md-6 col-sm-6">
|
<div class="col-lg-6 col-md-6 col-sm-6">
|
||||||
<div class="record-header clearfix">
|
<div class="record-header clearfix">
|
||||||
<div class="hidden-sm hidden-xs recordImage bgproject app-{$SELECTED_MENU_CATEGORY}">
|
<div class="hidden-sm hidden-xs recordImage bgproject app-{$SELECTED_MENU_CATEGORY}">
|
||||||
<div class="name"><span><strong> <i class="vicon-project"></i> </strong></span></div>
|
<div class="name"><span><strong> <i class="vicon-project"></i> </strong></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="recordBasicInfo">
|
<div class="recordBasicInfo">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<h4>
|
<h4>
|
||||||
<div class="recordLabel pushDown" title="{$RECORD->getName()}">
|
<div class="recordLabel pushDown" title="{$RECORD->getName()}">
|
||||||
{foreach item=NAME_FIELD from=$MODULE_MODEL->getNameFields()}
|
{foreach item=NAME_FIELD from=$MODULE_MODEL->getNameFields()}
|
||||||
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField($NAME_FIELD)}
|
{assign var=FIELD_MODEL value=$MODULE_MODEL->getField($NAME_FIELD)}
|
||||||
{if $FIELD_MODEL->getPermissions()}
|
{if $FIELD_MODEL->getPermissions()}
|
||||||
<span class="{$NAME_FIELD}">{$RECORD->get($NAME_FIELD)}</span>
|
<span class="{$NAME_FIELD}">{$RECORD->get($NAME_FIELD)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
|
{include file="DetailViewHeaderFieldsView.tpl"|vtemplate_path:$MODULE}
|
||||||
|
|
||||||
{*
|
{* Кнопка открытия папки проекта в Nextcloud *}
|
||||||
{assign var=RELATED_TO value=$RECORD->get('linktoaccountscontacts')}
|
<div class="info-row" style="margin-top: 10px;">
|
||||||
{assign var=CONTACT value=$RECORD->get('contactid')}
|
<button type="button" class="btn btn-info btn-sm" onclick="openProjectFolder('{$RECORD->getId()}', '{$RECORD->get("projectname")|escape:javascript}')" title="Открыть папку проекта в Nextcloud">
|
||||||
<div class="info-row row ">
|
<i class="fa fa-folder-open"></i> Папка в Nextcloud
|
||||||
{if !empty($RELATED_TO)}
|
</button>
|
||||||
<div class="col-lg-7 fieldLabel">
|
</div>
|
||||||
<span class="muted">
|
|
||||||
{$RECORD->getDisplayValue('linktoaccountscontacts')}
|
{* Подключаем Nextcloud Editor JS *}
|
||||||
</span>
|
<script type="text/javascript" src="layouts/v7/lib/nextcloud-editor.js"></script>
|
||||||
</div>
|
|
||||||
{elseif !empty($CONTACT)}
|
{*
|
||||||
<div class="info-row row ">
|
{assign var=RELATED_TO value=$RECORD->get('linktoaccountscontacts')}
|
||||||
<div class="col-lg-7 fieldLabel">
|
{assign var=CONTACT value=$RECORD->get('contactid')}
|
||||||
<span class="muted">
|
<div class="info-row row ">
|
||||||
{$RECORD->getDisplayValue('contactid')}</span>
|
{if !empty($RELATED_TO)}
|
||||||
</div>
|
<div class="col-lg-7 fieldLabel">
|
||||||
</div>
|
<span class="muted">
|
||||||
{/if}
|
{$RECORD->getDisplayValue('linktoaccountscontacts')}
|
||||||
</div>
|
</span>
|
||||||
*}
|
</div>
|
||||||
</div>
|
{elseif !empty($CONTACT)}
|
||||||
</div>
|
<div class="info-row row ">
|
||||||
</div>
|
<div class="col-lg-7 fieldLabel">
|
||||||
|
<span class="muted">
|
||||||
|
{$RECORD->getDisplayValue('contactid')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
*}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/strip}
|
{/strip}
|
||||||
Reference in New Issue
Block a user