✨ 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!
276 lines
10 KiB
PHP
276 lines
10 KiB
PHP
<?php
|
||
/**
|
||
* FilePathManager - Универсальный менеджер путей файлов
|
||
*
|
||
* Единая точка для генерации путей файлов в S3 для всех модулей CRM
|
||
* Поддерживает универсальную структуру: Documents/{ModuleName}/{RecordName}_{RecordId}/{FileName}_{DocumentId}.ext
|
||
*
|
||
* Примеры:
|
||
* - Project: Documents/Иванов_Против_ООО_123/Договор_456.pdf
|
||
* - Contacts: Documents/Contacts/Петров_Иван_789/Паспорт_101.pdf
|
||
* - Accounts: Documents/Accounts/ООО_Ромашка_555/Договор_666.docx
|
||
*
|
||
* @author AI Assistant
|
||
* @date 2025-10-22
|
||
*/
|
||
|
||
class FilePathManager {
|
||
private $adb;
|
||
private $prefix = 'crm2/CRM_Active_Files/Documents';
|
||
|
||
// Конфигурация полей для получения названия записи
|
||
private $moduleFieldMap = [
|
||
'Project' => ['field' => 'projectname', 'table' => 'vtiger_project', 'id' => 'projectid'],
|
||
'Contacts' => ['field' => 'CONCAT(firstname, " ", lastname)', 'table' => 'vtiger_contactdetails', 'id' => 'contactid'],
|
||
'Accounts' => ['field' => 'accountname', 'table' => 'vtiger_account', 'id' => 'accountid'],
|
||
'HelpDesk' => ['field' => 'title', 'table' => 'vtiger_troubletickets', 'id' => 'ticketid'],
|
||
'Invoice' => ['field' => 'subject', 'table' => 'vtiger_invoice', 'id' => 'invoiceid'],
|
||
'Leads' => ['field' => 'CONCAT(firstname, " ", lastname)', 'table' => 'vtiger_leaddetails', 'id' => 'leadid'],
|
||
];
|
||
|
||
public function __construct() {
|
||
global $adb;
|
||
$this->adb = $adb;
|
||
}
|
||
|
||
/**
|
||
* Санитизация имени файла/папки
|
||
* Заменяет проблемные символы на подчеркивания
|
||
*
|
||
* @param string $name Исходное имя
|
||
* @return string Санитизированное имя
|
||
*/
|
||
public function sanitizeFileName($name) {
|
||
if (empty($name)) {
|
||
return '';
|
||
}
|
||
|
||
// Декодируем HTML entities
|
||
$name = html_entity_decode($name, ENT_QUOTES, 'UTF-8');
|
||
|
||
// Заменяем проблемные символы (включая №)
|
||
$name = str_replace(["/", "\\", ":", "*", "?", "\"", "<", ">", "|", "№"], '_', $name);
|
||
|
||
// Заменяем все пробелы и запятые на подчеркивания
|
||
$name = preg_replace('/[\s,]+/', '_', $name);
|
||
|
||
// Убираем повторяющиеся подчеркивания
|
||
$name = preg_replace('/_+/', '_', $name);
|
||
|
||
return trim($name, '_');
|
||
}
|
||
|
||
/**
|
||
* Получить название записи из базы данных
|
||
*
|
||
* @param string $module Название модуля
|
||
* @param int $recordId ID записи
|
||
* @return string|null Название записи или null
|
||
*/
|
||
public function getRecordName($module, $recordId) {
|
||
if (!isset($this->moduleFieldMap[$module])) {
|
||
return null;
|
||
}
|
||
|
||
$config = $this->moduleFieldMap[$module];
|
||
|
||
try {
|
||
$query = "SELECT {$config['field']} as name FROM {$config['table']} WHERE {$config['id']} = ?";
|
||
$result = $this->adb->pquery($query, [$recordId]);
|
||
|
||
if ($this->adb->num_rows($result) > 0) {
|
||
$name = $this->adb->query_result($result, 0, 'name');
|
||
return $this->sanitizeFileName($name);
|
||
}
|
||
} catch (Exception $e) {
|
||
error_log("FilePathManager: Error getting record name for $module:$recordId - " . $e->getMessage());
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Сгенерировать путь к папке записи
|
||
*
|
||
* @param string $module Название модуля
|
||
* @param int $recordId ID записи
|
||
* @param string|null $recordName Название записи (опционально, будет получено из БД)
|
||
* @return string Путь к папке
|
||
*/
|
||
public function getRecordFolderPath($module, $recordId, $recordName = null) {
|
||
// Если название не передано, получаем из базы
|
||
if ($recordName === null) {
|
||
$recordName = $this->getRecordName($module, $recordId);
|
||
} else {
|
||
$recordName = $this->sanitizeFileName($recordName);
|
||
}
|
||
|
||
// Формируем имя папки: ModuleName/название_ID
|
||
$folderName = $recordName ? "{$recordName}_{$recordId}" : "{$module}_{$recordId}";
|
||
$folderName = "{$module}/{$folderName}";
|
||
|
||
return "{$this->prefix}/{$folderName}";
|
||
}
|
||
|
||
/**
|
||
* Сгенерировать полный путь к файлу
|
||
*
|
||
* @param string $module Название модуля
|
||
* @param int $recordId ID записи
|
||
* @param int $documentId ID документа
|
||
* @param string $fileName Имя файла
|
||
* @param string|null $documentTitle Название документа (опционально)
|
||
* @param string|null $recordName Название записи (опционально)
|
||
* @return string Полный путь к файлу
|
||
*/
|
||
public function getFilePath($module, $recordId, $documentId, $fileName, $documentTitle = null, $recordName = null) {
|
||
// Получаем путь к папке
|
||
$folderPath = $this->getRecordFolderPath($module, $recordId, $recordName);
|
||
|
||
// Извлекаем расширение
|
||
$extension = $this->extractExtension($fileName);
|
||
|
||
// Формируем имя файла
|
||
if ($documentTitle) {
|
||
$sanitizedTitle = $this->sanitizeFileName($documentTitle);
|
||
$newFileName = "{$sanitizedTitle}_{$documentId}";
|
||
} else {
|
||
$newFileName = "document_{$documentId}";
|
||
}
|
||
|
||
// Добавляем расширение
|
||
if ($extension) {
|
||
$newFileName .= ".{$extension}";
|
||
}
|
||
|
||
return "{$folderPath}/{$newFileName}";
|
||
}
|
||
|
||
/**
|
||
* Извлечь расширение файла
|
||
*
|
||
* @param string $fileName Имя файла
|
||
* @return string|null Расширение без точки
|
||
*/
|
||
private function extractExtension($fileName) {
|
||
$fileName = basename($fileName);
|
||
$dotPos = strrpos($fileName, '.');
|
||
|
||
if ($dotPos !== false && $dotPos < strlen($fileName) - 1) {
|
||
return strtolower(substr($fileName, $dotPos + 1));
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Проверить, поддерживается ли модуль
|
||
*
|
||
* @param string $module Название модуля
|
||
* @return bool
|
||
*/
|
||
public function isModuleSupported($module) {
|
||
return isset($this->moduleFieldMap[$module]);
|
||
}
|
||
|
||
/**
|
||
* Получить список поддерживаемых модулей
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getSupportedModules() {
|
||
return array_keys($this->moduleFieldMap);
|
||
}
|
||
|
||
/**
|
||
* Парсить путь файла и получить информацию
|
||
* Поддерживает как старую, так и новую структуру
|
||
*
|
||
* @param string $filePath Путь к файлу
|
||
* @return array|null ['module' => string, 'recordId' => int, 'documentId' => int, 'fileName' => string] или null
|
||
*/
|
||
public function parseFilePath($filePath) {
|
||
// Убираем домен и bucket если есть
|
||
$filePath = preg_replace('#^https?://[^/]+/[^/]+/#', '', $filePath);
|
||
|
||
// Убираем префикс
|
||
$filePath = str_replace($this->prefix . '/', '', $filePath);
|
||
|
||
// Проверяем структуру пути
|
||
$parts = explode('/', $filePath);
|
||
$partsCount = count($parts);
|
||
|
||
// Новая структура с модулем: Module/название_recordId/файл_documentId.ext (3 части)
|
||
if ($partsCount == 3 && $this->isModuleSupported($parts[0])) {
|
||
$module = $parts[0];
|
||
$folderName = $parts[1];
|
||
$fileName = $parts[2];
|
||
|
||
// Извлекаем recordId из имени папки (название_ID)
|
||
if (preg_match('/_(\d+)$/', $folderName, $idMatch)) {
|
||
$recordId = (int)$idMatch[1];
|
||
} else {
|
||
return null;
|
||
}
|
||
|
||
// Извлекаем documentId из имени файла
|
||
if (preg_match('/_(\d+)\.[^.]+$/', $fileName, $docMatch)) {
|
||
$documentId = (int)$docMatch[1];
|
||
} else {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'module' => $module,
|
||
'recordId' => $recordId,
|
||
'documentId' => $documentId,
|
||
'fileName' => $fileName
|
||
];
|
||
}
|
||
|
||
// Project структура: название_recordId/файл_documentId.ext (2 части)
|
||
if ($partsCount == 2) {
|
||
$folderName = $parts[0];
|
||
$fileName = $parts[1];
|
||
|
||
// Извлекаем recordId из имени папки (название_ID)
|
||
if (preg_match('/_(\d+)$/', $folderName, $idMatch)) {
|
||
$recordId = (int)$idMatch[1];
|
||
} else {
|
||
return null;
|
||
}
|
||
|
||
// Извлекаем documentId из имени файла
|
||
if (preg_match('/_(\d+)\.[^.]+$/', $fileName, $docMatch)) {
|
||
$documentId = (int)$docMatch[1];
|
||
} else {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'module' => 'Project',
|
||
'recordId' => $recordId,
|
||
'documentId' => $documentId,
|
||
'fileName' => $fileName
|
||
];
|
||
}
|
||
|
||
// Старая структура: documentId/файл.ext
|
||
if (preg_match('#^(\d+)/([^/]+)$#', $filePath, $matches)) {
|
||
$documentId = (int)$matches[1];
|
||
$fileName = $matches[2];
|
||
|
||
return [
|
||
'module' => null,
|
||
'recordId' => null,
|
||
'documentId' => $documentId,
|
||
'fileName' => $fileName,
|
||
'isOldStructure' => true
|
||
];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|