✨ 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
9.9 KiB
Plaintext
276 lines
9.9 KiB
Plaintext
<?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;
|
||
}
|
||
}
|
||
?>
|