🚀 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!
This commit is contained in:
275
include/Storage/S3StorageService.php.backup
Normal file
275
include/Storage/S3StorageService.php.backup
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
/**
|
||||
* S3 Storage Service
|
||||
* Сервис для работы с S3 хранилищем в vTiger CRM
|
||||
*
|
||||
* Функции:
|
||||
* - Загрузка файлов в S3 с retry механизмом
|
||||
* - Получение presigned URLs для скачивания
|
||||
* - Логирование ошибок
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @date 2025-09-20
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../crm_extensions/file_storage/S3Client.php';
|
||||
|
||||
class S3StorageService {
|
||||
private $s3Client;
|
||||
private $bucket;
|
||||
private $prefix;
|
||||
private $config;
|
||||
|
||||
public function __construct() {
|
||||
// Загружаем конфигурацию 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 с retry механизмом
|
||||
*
|
||||
* @param string $tmpFile Временный файл на сервере
|
||||
* @param int $notesId ID записи документа
|
||||
* @param string $filename Имя файла
|
||||
* @param int $maxRetries Максимальное количество попыток
|
||||
* @return array Результат загрузки
|
||||
* @throws Exception При неудачной загрузке
|
||||
*/
|
||||
public function put($tmpFile, $notesId, $filename, $maxRetries = 3) {
|
||||
$key = $this->prefix . '/' . $notesId . '/' . $filename;
|
||||
|
||||
$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')
|
||||
]
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user