Files
crm.clientright.ru/crm_extensions/file_storage/S3Client.php
Fedor 840acca51a feat(documents): дедупликация documents_meta и исправление field_label
- Исправлен N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js: использовать uploads_field_labels[0] вместо [grp]
- Создан SQL_CLAIMSAVE_FIXED_NEW_FLOW_DEDUP.sql с дедупликацией documents_meta
- Создан SQL_CLEANUP_DOCUMENTS_META_DUPLICATES.sql для очистки существующих дубликатов
- Создан полный уникальный индекс idx_document_texts_hash_unique на document_texts(file_hash)
- Добавлен SESSION_LOG_2025-11-28_documents_dedup.md с описанием всех изменений

Fixes:
- field_label теперь корректно отображает 'Переписка' вместо 'group-2'
- documents_meta не накапливает дубликаты при повторных сохранениях
- ON CONFLICT (file_hash) теперь работает для document_texts
2025-11-28 18:16:53 +03:00

216 lines
7.0 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-Compatible Storage Client
* Клиент для работы с S3-совместимым хранилищем (TWC Storage)
*/
require_once __DIR__ . '/../vendor/autoload.php';
use Aws\S3\S3Client as AwsS3Client;
use Aws\Exception\AwsException;
class S3Client {
private $client;
private $bucket;
public function __construct($config) {
$this->bucket = $config['bucket'];
// Настройка для российского S3-совместимого хранилища
$this->client = new AwsS3Client([
'version' => $config['version'],
'region' => $config['region'],
'endpoint' => $config['endpoint'],
'use_path_style_endpoint' => $config['use_path_style_endpoint'],
'credentials' => [
'key' => $config['key'],
'secret' => $config['secret'],
],
'http' => [
'verify' => true,
]
]);
}
/**
* Загрузка файла в S3
*/
public function uploadFile($localPath, $s3Key, $options = []) {
try {
$putObjectParams = [
'Bucket' => $this->bucket,
'Key' => $s3Key,
'SourceFile' => $localPath,
];
// Добавляем ContentType если указан
if (isset($options['ContentType'])) {
$putObjectParams['ContentType'] = $options['ContentType'];
} else {
$putObjectParams['ContentType'] = $this->getMimeType($localPath);
}
// Добавляем метаданные если указаны
if (isset($options['Metadata']) && is_array($options['Metadata'])) {
// AWS SDK ожидает все значения метаданных как строки
$metadata = [];
foreach ($options['Metadata'] as $key => $value) {
$metadata[$key] = (string)$value;
}
$putObjectParams['Metadata'] = $metadata;
}
$result = $this->client->putObject($putObjectParams);
return [
'success' => true,
'url' => $this->getPublicUrl($s3Key),
's3_key' => $s3Key,
'etag' => $result['ETag']
];
} catch (AwsException $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Создание временной ссылки для скачивания
* @param string $s3Key S3 ключ файла
* @param mixed $expiresIn Время жизни URL в секундах (число) или строка типа '+10 minutes'
*/
public function getPresignedUrl($s3Key, $expiresIn = 3600) {
try {
// Преобразуем строку TTL в секунды, если нужно
if (is_string($expiresIn)) {
// Если строка начинается с '+', используем её как есть для strtotime
if (strpos($expiresIn, '+') === 0) {
$expiresIn = strtotime($expiresIn) - time();
} else {
// Иначе пытаемся распарсить как число секунд
$expiresIn = (int)$expiresIn;
}
}
// Минимум 60 секунд, максимум 7 дней
$expiresIn = max(60, min($expiresIn, 604800));
$cmd = $this->client->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $s3Key
]);
$request = $this->client->createPresignedRequest($cmd, "+{$expiresIn} seconds");
return [
'success' => true,
'url' => (string) $request->getUri()
];
} catch (AwsException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'error_code' => $e->getAwsErrorCode(),
'request_id' => $e->getAwsRequestId()
];
}
}
/**
* Удаление файла из S3
*/
public function deleteObject($s3Key) {
try {
$result = $this->client->deleteObject([
'Bucket' => $this->bucket,
'Key' => $s3Key
]);
return [
'success' => true,
'deleted_key' => $s3Key
];
} catch (AwsException $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Скачивание файла во временную папку
*/
public function downloadToTemp($s3Key) {
$tempFile = sys_get_temp_dir() . '/' . uniqid('s3_download_') . '_' . basename($s3Key);
try {
$this->client->getObject([
'Bucket' => $this->bucket,
'Key' => $s3Key,
'SaveAs' => $tempFile
]);
return $tempFile;
} catch (AwsException $e) {
throw new Exception('S3 download failed: ' . $e->getMessage());
}
}
/**
* Проверка существования файла
*/
public function fileExists($s3Key) {
try {
$this->client->headObject([
'Bucket' => $this->bucket,
'Key' => $s3Key
]);
return true;
} catch (AwsException $e) {
return false;
}
}
/**
* Получение публичного URL
*/
private function getPublicUrl($s3Key) {
return $this->client->getObjectUrl($this->bucket, $s3Key);
}
/**
* Определение MIME типа файла
*/
private function getMimeType($filePath) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
return $mimeType ?: 'application/octet-stream';
}
/**
* Генерация ключа для S3 на основе CRM данных
*/
public function generateS3Key($crmData, $isNewVersion = false) {
$module = $crmData['module'] ?? 'Documents';
$recordId = $crmData['record_id'] ?? 'unknown';
$fileName = $crmData['file_name'] ?? 'file';
$basePath = strtolower($module) . '/' . $recordId;
if ($isNewVersion) {
$timestamp = date('Y-m-d_H-i-s');
$fileName = pathinfo($fileName, PATHINFO_FILENAME) . '_v' . $timestamp . '.' . pathinfo($fileName, PATHINFO_EXTENSION);
}
return $basePath . '/' . $fileName;
}
}