281 lines
10 KiB
PHP
281 lines
10 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* Klient HTTP do komunikacji z Python FastAPI serwisem plików.
|
||
*
|
||
* Serwis działa na 127.0.0.1:8001 i jest dostępny wyłącznie z serwera.
|
||
* PHP komunikuje się z nim przez cURL, przekazując klucz API w nagłówku X-API-Key.
|
||
*
|
||
* Konfiguracja (zmienne środowiskowe lub bezpośrednio w konstruktorze):
|
||
* FILE_API_URL – bazowy URL serwisu, domyślnie http://127.0.0.1:8001
|
||
* FILE_API_KEY – klucz API (musi być taki sam jak w api/.env)
|
||
*
|
||
* Przykład użycia:
|
||
* $client = get_file_api_client();
|
||
* $result = $client->upload('admin_chat', $_FILES['file']['tmp_name'],
|
||
* $_FILES['file']['name'], $_FILES['file']['type']);
|
||
* $filePath = $result['path']; // np. "admin_chat/abc123.jpg"
|
||
*/
|
||
|
||
class FileApiClient
|
||
{
|
||
private string $baseUrl;
|
||
private string $apiKey;
|
||
private int $timeoutSeconds;
|
||
|
||
public function __construct(
|
||
string $baseUrl = 'http://127.0.0.1:8001',
|
||
string $apiKey = '',
|
||
int $timeoutSeconds = 30
|
||
) {
|
||
$this->baseUrl = rtrim($baseUrl, '/');
|
||
$this->apiKey = $apiKey !== '' ? $apiKey : (string)(getenv('FILE_API_KEY') ?: '');
|
||
$this->timeoutSeconds = $timeoutSeconds;
|
||
}
|
||
|
||
private static function stringifyDetail($value): string
|
||
{
|
||
if (is_string($value)) {
|
||
return trim($value);
|
||
}
|
||
|
||
if (is_array($value)) {
|
||
$parts = [];
|
||
foreach ($value as $item) {
|
||
if (is_string($item)) {
|
||
$parts[] = trim($item);
|
||
continue;
|
||
}
|
||
if (is_array($item)) {
|
||
$loc = '';
|
||
if (!empty($item['loc']) && is_array($item['loc'])) {
|
||
$loc = implode('.', array_map('strval', $item['loc']));
|
||
}
|
||
$msg = isset($item['msg']) ? trim((string)$item['msg']) : '';
|
||
$type = isset($item['type']) ? trim((string)$item['type']) : '';
|
||
$piece = '';
|
||
if ($loc !== '') {
|
||
$piece .= '[' . $loc . '] ';
|
||
}
|
||
if ($msg !== '') {
|
||
$piece .= $msg;
|
||
}
|
||
if ($type !== '') {
|
||
$piece .= ($piece !== '' ? ' ' : '') . '(' . $type . ')';
|
||
}
|
||
if ($piece !== '') {
|
||
$parts[] = $piece;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($parts)) {
|
||
return implode(' | ', $parts);
|
||
}
|
||
|
||
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
if (is_string($json) && $json !== '') {
|
||
return $json;
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Prześlij plik do serwisu i zapisz na dysku.
|
||
*
|
||
* @param string $endpoint Endpoint routera, np. 'admin_chat' lub 'admin_tasks'
|
||
* @param string $tmpPath Ścieżka do tymczasowego pliku z $_FILES[...]['tmp_name']
|
||
* @param string $fileName Oryginalna nazwa pliku
|
||
* @param string $mimeType Typ MIME
|
||
* @return array{stored_name: string, original_name: string, mime: string, size: int, path: string}
|
||
* @throws RuntimeException Jeśli upload się nie powiódł
|
||
*/
|
||
public function upload(
|
||
string $endpoint,
|
||
string $tmpPath,
|
||
string $fileName,
|
||
string $mimeType
|
||
): array {
|
||
if (!is_uploaded_file($tmpPath)) {
|
||
throw new RuntimeException('Nieprawidłowy plik do przesłania (nie pochodzi z $_FILES)');
|
||
}
|
||
|
||
$url = $this->baseUrl . '/' . ltrim($endpoint, '/') . '/upload';
|
||
|
||
$ch = curl_init($url);
|
||
if ($ch === false) {
|
||
throw new RuntimeException('Nie można zainicjować cURL');
|
||
}
|
||
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $this->apiKey],
|
||
CURLOPT_POSTFIELDS => [
|
||
'file' => new CURLFile($tmpPath, $mimeType, $fileName),
|
||
],
|
||
// Wyłącz weryfikację SSL (localhost nie ma certyfikatu)
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => 0,
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$curlErr = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($curlErr !== '') {
|
||
throw new RuntimeException('Błąd połączenia z serwisem plików: ' . $curlErr);
|
||
}
|
||
|
||
$data = json_decode((string)$response, true);
|
||
|
||
if ($httpCode !== 200 || !is_array($data) || empty($data['success'])) {
|
||
$detail = '';
|
||
if (is_array($data)) {
|
||
$detail = self::stringifyDetail($data['detail'] ?? '');
|
||
if ($detail === '') {
|
||
$detail = self::stringifyDetail($data['error'] ?? '');
|
||
}
|
||
}
|
||
if ($detail === '') {
|
||
$raw = trim((string)$response);
|
||
if ($raw !== '') {
|
||
$detail = $raw;
|
||
}
|
||
}
|
||
if ($detail === '') {
|
||
$detail = 'nieznany błąd';
|
||
}
|
||
|
||
$exceptionCode = ($httpCode >= 400 && $httpCode <= 599) ? $httpCode : 0;
|
||
throw new RuntimeException('Serwis plików zwrócił błąd: ' . $detail . ' (HTTP ' . $httpCode . ')', $exceptionCode);
|
||
}
|
||
|
||
return (array)($data['data'] ?? []);
|
||
}
|
||
|
||
/**
|
||
* Pobierz plik z serwisu jako strumień binarny i przekaż go do klienta.
|
||
* Metoda ustawia nagłówki HTTP i wypisuje ciało pliku – użyj jej tuż przed exit().
|
||
*
|
||
* @param string $endpoint Endpoint routera, np. 'admin_chat'
|
||
* @param string $storedName Nazwa pliku na dysku, np. 'abc123.jpg'
|
||
* @param bool $inline true = wyświetl w przeglądarce, false = pobierz
|
||
* @throws RuntimeException Jeśli plik nie istnieje lub wystąpił błąd
|
||
*/
|
||
public function proxyFile(string $endpoint, string $storedName, bool $inline = false): void
|
||
{
|
||
$url = $this->baseUrl . '/' . ltrim($endpoint, '/') . '/file/' . rawurlencode($storedName);
|
||
if ($inline) {
|
||
$url .= '?inline=1';
|
||
}
|
||
|
||
$ch = curl_init($url);
|
||
if ($ch === false) {
|
||
throw new RuntimeException('Nie można zainicjować cURL');
|
||
}
|
||
|
||
// Strumieniowanie bezpośrednio na wyjście PHP – bez buforowania całego pliku w RAM
|
||
$headersSent = false;
|
||
$responseHeaders = [];
|
||
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $this->apiKey],
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => 0,
|
||
// Zbierz nagłówki odpowiedzi
|
||
CURLOPT_HEADERFUNCTION => static function ($ch, string $header) use (&$responseHeaders): int {
|
||
$responseHeaders[] = rtrim($header, "\r\n");
|
||
return strlen($header);
|
||
},
|
||
// Każdy chunk ciała trafia bezpośrednio na wyjście
|
||
CURLOPT_WRITEFUNCTION => static function ($ch, string $data) use (&$headersSent, &$responseHeaders): int {
|
||
if (!$headersSent) {
|
||
foreach ($responseHeaders as $h) {
|
||
if (stripos($h, 'Content-Type:') === 0
|
||
|| stripos($h, 'Content-Disposition:') === 0
|
||
|| stripos($h, 'Content-Length:') === 0
|
||
) {
|
||
header($h);
|
||
}
|
||
}
|
||
$headersSent = true;
|
||
}
|
||
echo $data;
|
||
return strlen($data);
|
||
},
|
||
]);
|
||
|
||
curl_exec($ch);
|
||
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$curlErr = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($curlErr !== '') {
|
||
throw new RuntimeException('Błąd połączenia z serwisem plików: ' . $curlErr);
|
||
}
|
||
|
||
if ($httpCode === 404) {
|
||
throw new RuntimeException('Plik nie istnieje', 404);
|
||
}
|
||
|
||
if ($httpCode !== 200) {
|
||
throw new RuntimeException('Błąd pobierania pliku (HTTP ' . $httpCode . ')');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Usuń plik z serwisu (i z dysku).
|
||
*
|
||
* @param string $endpoint Endpoint routera, np. 'admin_chat'
|
||
* @param string $storedName Nazwa pliku na dysku
|
||
* @return bool true jeśli plik istniał i został usunięty
|
||
*/
|
||
public function deleteFile(string $endpoint, string $storedName): bool
|
||
{
|
||
$url = $this->baseUrl . '/' . ltrim($endpoint, '/') . '/file/' . rawurlencode($storedName);
|
||
|
||
$ch = curl_init($url);
|
||
if ($ch === false) {
|
||
return false;
|
||
}
|
||
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
||
CURLOPT_TIMEOUT => 10,
|
||
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $this->apiKey],
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => 0,
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
$data = json_decode((string)$response, true);
|
||
return $httpCode === 200 && is_array($data) && !empty($data['success']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Singleton – zwraca skonfigurowany FileApiClient.
|
||
* Konfiguracja przez zmienne środowiskowe FILE_API_URL i FILE_API_KEY.
|
||
*/
|
||
function get_file_api_client(): FileApiClient
|
||
{
|
||
static $instance = null;
|
||
if ($instance === null) {
|
||
$baseUrl = (string)(getenv('FILE_API_URL') ?: 'http://127.0.0.1:8001');
|
||
$apiKey = (string)(getenv('FILE_API_KEY') ?: '');
|
||
$instance = new FileApiClient($baseUrl, $apiKey);
|
||
}
|
||
return $instance;
|
||
}
|