- Исправлен 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
216 lines
7.0 KiB
PHP
216 lines
7.0 KiB
PHP
<?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;
|
||
}
|
||
}
|