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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|