togethere.cloud/public_html/includes/file_api_client.php

281 lines
10 KiB
PHP
Raw Permalink 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
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;
}