Files
crm.clientright.ru/include/Storage/S3StorageService.php
Fedor 9245768987 🚀 CRM Files Migration & Real-time Features
 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!
2025-10-24 19:59:28 +03:00

309 lines
12 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
/**
* S3 Storage Service
* Сервис для работы с S3 хранилищем в vTiger CRM
*
* Функции:
* - Загрузка файлов в S3 с retry механизмом
* - Получение presigned URLs для скачивания
* - Логирование ошибок
* - Поддержка универсальной структуры путей через FilePathManager
*
* @author AI Assistant
* @date 2025-10-22 (updated)
*/
require_once __DIR__ . '/../../crm_extensions/file_storage/S3Client.php';
require_once __DIR__ . '/../../crm_extensions/file_storage/FilePathManager.php';
class S3StorageService {
private $s3Client;
private $bucket;
private $prefix;
private $config;
private $filePathManager;
public function __construct() {
// Инициализируем FilePathManager
$this->filePathManager = new FilePathManager();
// Загружаем конфигурацию S3
try {
file_put_contents('logs/debug.log', '[' . date('Y-m-d H:i:s') . '] S3StorageService: Loading config...' . PHP_EOL, FILE_APPEND);
$this->config = require __DIR__ . '/../../crm_extensions/file_storage/config.php';
file_put_contents('logs/debug.log', '[' . date('Y-m-d H:i:s') . '] S3StorageService: Config loaded, S3 key=' . $this->config['s3']['key'] . PHP_EOL, FILE_APPEND);
$this->s3Client = new S3Client($this->config['s3']);
$this->bucket = $this->config['s3']['bucket'];
$this->prefix = 'crm2/CRM_Active_Files/Documents';
file_put_contents('logs/debug.log', '[' . date('Y-m-d H:i:s') . '] S3StorageService: Constructor completed successfully' . PHP_EOL, FILE_APPEND);
} catch (Exception $e) {
file_put_contents('logs/debug.log', '[' . date('Y-m-d H:i:s') . '] S3StorageService ERROR: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
throw $e;
}
// Создаем папку для логов если её нет
$logDir = __DIR__ . '/../../logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
}
/**
* Загрузка файла в S3 с поддержкой универсальной структуры
*
* @param string $tmpFile Временный файл на сервере
* @param int $notesId ID записи документа
* @param string $filename Имя файла
* @param int $maxRetries Максимальное количество попыток
* @param array $context Контекст: ['module' => string, 'recordId' => int, 'recordName' => string, 'documentTitle' => string]
* @return array Результат загрузки
* @throws Exception При неудачной загрузке
*/
public function put($tmpFile, $notesId, $filename, $maxRetries = 3, $context = []) {
// Определяем путь к файлу
if (!empty($context['module']) && !empty($context['recordId'])) {
// Новая универсальная структура
$module = $context['module'];
$recordId = $context['recordId'];
$recordName = $context['recordName'] ?? null;
$documentTitle = $context['documentTitle'] ?? null;
$key = $this->filePathManager->getFilePath($module, $recordId, $notesId, $filename, $documentTitle, $recordName);
$this->logInfo("Using universal path structure: module=$module, recordId=$recordId");
} else {
// Старая структура (обратная совместимость)
$key = $this->prefix . '/' . $notesId . '/' . $filename;
$this->logInfo("Using legacy path structure");
}
$this->logInfo("Starting S3 upload: key=$key, file=$tmpFile");
for ($i = 0; $i < $maxRetries; $i++) {
try {
$this->logInfo("S3 upload attempt " . ($i + 1) . "/$maxRetries");
// Проверяем, что временный файл существует
if (!file_exists($tmpFile)) {
throw new Exception("Temporary file not found: $tmpFile");
}
// Получаем MIME тип
$mimeType = $this->getMimeType($tmpFile);
// Загружаем файл в S3
$result = $this->s3Client->uploadFile($tmpFile, $key, [
'ContentType' => $mimeType,
'Metadata' => [
'notesId' => (string)$notesId,
'originalName' => (string)$filename,
'uploadedAt' => (string)date('Y-m-d H:i:s'),
'module' => !empty($context['module']) ? (string)$context['module'] : '',
'recordId' => !empty($context['recordId']) ? (string)$context['recordId'] : ''
]
]);
if ($result['success']) {
$this->logInfo("S3 upload successful: key=$key, etag=" . $result['etag']);
return [
'key' => $key,
'etag' => $result['etag'],
'url' => $result['url'],
'bucket' => $this->bucket,
'size' => filesize($tmpFile),
'mimeType' => $mimeType
];
} else {
throw new Exception("S3 upload failed: " . ($result['error'] ?? 'Unknown error'));
}
} catch (Exception $e) {
$this->logError("S3 upload attempt " . ($i + 1) . " failed: " . $e->getMessage());
if ($i < $maxRetries - 1) {
$delay = pow(2, $i); // 1s, 2s, 4s
$this->logInfo("Retrying in {$delay}s...");
sleep($delay);
}
}
}
$errorMsg = "S3 upload failed after $maxRetries attempts for key: $key";
$this->logError($errorMsg);
throw new Exception($errorMsg);
}
/**
* Получение presigned URL для скачивания
*
* @param string $key S3 ключ файла
* @param string $ttl Время жизни URL
* @return string Presigned URL
*/
public function presignGet($key, $ttl = '+10 minutes') {
try {
$this->logInfo("Generating presigned URL for key: $key, TTL: $ttl");
$presignedUrl = $this->s3Client->getPresignedUrl($key, $ttl);
// Если getPresignedUrl возвращает массив, берем URL
if (is_array($presignedUrl)) {
$presignedUrl = $presignedUrl['url'] ?? $presignedUrl[0] ?? '';
}
$this->logInfo("Presigned URL generated successfully");
return $presignedUrl;
} catch (Exception $e) {
$this->logError("Failed to generate presigned URL for key $key: " . $e->getMessage());
throw $e;
}
}
/**
* Удаление файла из S3
*
* @param string $key S3 ключ файла
* @return bool Результат удаления
*/
public function delete($key) {
try {
$this->logInfo("Deleting S3 object: key=$key");
$result = $this->s3Client->deleteObject($key);
if ($result['success']) {
$this->logInfo("S3 object deleted successfully: key=$key");
return true;
} else {
$this->logError("Failed to delete S3 object: key=$key, error=" . ($result['error'] ?? 'Unknown error'));
return false;
}
} catch (Exception $e) {
$this->logError("Exception while deleting S3 object $key: " . $e->getMessage());
return false;
}
}
/**
* Проверка существования файла в S3
*
* @param string $key S3 ключ файла
* @return bool Файл существует
*/
public function exists($key) {
try {
return $this->s3Client->fileExists($key);
} catch (Exception $e) {
$this->logError("Error checking S3 object existence $key: " . $e->getMessage());
return false;
}
}
/**
* Получение MIME типа файла
*
* @param string $filePath Путь к файлу
* @return string MIME тип
*/
private function getMimeType($filePath) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
// Fallback для случаев, когда finfo не работает
if (!$mimeType) {
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$mimeTypes = [
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'txt' => 'text/plain',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif'
];
$mimeType = isset($mimeTypes[$extension]) ? $mimeTypes[$extension] : 'application/octet-stream';
}
return $mimeType;
}
/**
* Логирование информационных сообщений
*
* @param string $message Сообщение
*/
private function logInfo($message) {
$this->log('INFO', $message);
}
/**
* Логирование ошибок
*
* @param string $message Сообщение об ошибке
*/
private function logError($message) {
$this->log('ERROR', $message);
}
/**
* Общее логирование
*
* @param string $level Уровень лога
* @param string $message Сообщение
*/
private function log($level, $message) {
$logFile = __DIR__ . '/../../logs/s3_storage.log';
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[$timestamp] [$level] $message\n";
file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}
/**
* Получение конфигурации S3
*
* @return array Конфигурация
*/
public function getConfig() {
return $this->config['s3'];
}
/**
* Получение префикса для S3 ключей
*
* @return string Префикс
*/
public function getPrefix() {
return $this->prefix;
}
/**
* Получение имени bucket'а
*
* @return string Имя bucket'а
*/
public function getBucket() {
return $this->bucket;
}
/**
* Получение экземпляра FilePathManager
*
* @return FilePathManager
*/
public function getFilePathManager() {
return $this->filePathManager;
}
}
?>