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