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; }