2025-09-26 10:43:05 +03:00
|
|
|
|
<?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()
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Создание временной ссылки для скачивания
|
2025-11-28 18:16:53 +03:00
|
|
|
|
* @param string $s3Key S3 ключ файла
|
|
|
|
|
|
* @param mixed $expiresIn Время жизни URL в секундах (число) или строка типа '+10 minutes'
|
2025-09-26 10:43:05 +03:00
|
|
|
|
*/
|
|
|
|
|
|
public function getPresignedUrl($s3Key, $expiresIn = 3600) {
|
|
|
|
|
|
try {
|
2025-11-28 18:16:53 +03:00
|
|
|
|
// Преобразуем строку 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));
|
|
|
|
|
|
|
2025-09-26 10:43:05 +03:00
|
|
|
|
$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,
|
2025-11-28 18:16:53 +03:00
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
|
'error_code' => $e->getAwsErrorCode(),
|
|
|
|
|
|
'request_id' => $e->getAwsRequestId()
|
2025-09-26 10:43:05 +03:00
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Удаление файла из 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|