feat: add comprehensive framework features and deployment improvements
Major additions: - Storage abstraction layer with filesystem and in-memory implementations - Gitea API integration with MCP tools for repository management - Console dialog mode with interactive command execution - WireGuard VPN DNS fix implementation and documentation - HTTP client streaming response support - Router generic result type - Parameter type validator for framework core Framework enhancements: - Console command registry improvements - Console dialog components - Method signature analyzer updates - Route mapper refinements - MCP server and tool mapper updates - Queue job chain and dependency commands - Discovery tokenizer improvements Infrastructure: - Deployment architecture documentation - Ansible playbook updates for WireGuard client regeneration - Production environment configuration updates - Docker Compose local configuration updates - Remove obsolete docker-compose.yml (replaced by environment-specific configs) Documentation: - PERMISSIONS.md for access control guidelines - WireGuard DNS fix implementation details - Console dialog mode usage guide - Deployment architecture overview Testing: - Multi-purpose attribute tests - Gitea Actions integration tests (typed and untyped)
This commit is contained in:
@@ -58,6 +58,148 @@ final readonly class CurlHttpClient implements HttpClient
|
||||
// Wrap any exception in CurlExecutionFailed for backward compatibility
|
||||
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send request with streaming response to destination
|
||||
*
|
||||
* Streams HTTP response directly to a writable stream resource.
|
||||
* Useful for large file downloads without loading entire response into memory.
|
||||
*
|
||||
* @param ClientRequest $request HTTP request to send
|
||||
* @param resource $destination Writable stream resource (e.g., fopen('file.txt', 'w'))
|
||||
* @return StreamingResponse Response with headers and status, but no body (streamed to destination)
|
||||
* @throws CurlExecutionFailed If request execution fails
|
||||
*/
|
||||
public function sendStreaming(ClientRequest $request, $destination): StreamingResponse
|
||||
{
|
||||
if (! is_resource($destination)) {
|
||||
throw new \InvalidArgumentException('Destination must be a valid stream resource');
|
||||
}
|
||||
|
||||
$handle = new Handle();
|
||||
|
||||
try {
|
||||
// Build options using HandleOption enum
|
||||
$options = $this->requestBuilder->buildOptions($request);
|
||||
|
||||
// Remove CURLOPT_RETURNTRANSFER (we're streaming to destination)
|
||||
unset($options[\App\Framework\HttpClient\Curl\HandleOption::ReturnTransfer->value]);
|
||||
|
||||
// Handle authentication
|
||||
if ($request->options->auth !== null) {
|
||||
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
|
||||
|
||||
if ($authResult->headers !== $request->headers) {
|
||||
$updatedRequest = $request->with(['headers' => $authResult->headers]);
|
||||
$options = $this->requestBuilder->buildOptions($updatedRequest);
|
||||
unset($options[\App\Framework\HttpClient\Curl\HandleOption::ReturnTransfer->value]);
|
||||
}
|
||||
|
||||
if (! empty($authResult->curlOptions)) {
|
||||
$options = array_replace($options, $authResult->curlOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable header capture for streaming
|
||||
$headerBuffer = '';
|
||||
$headerFunction = function ($ch, $header) use (&$headerBuffer) {
|
||||
$headerBuffer .= $header;
|
||||
return strlen($header);
|
||||
};
|
||||
$options[\App\Framework\HttpClient\Curl\HandleOption::HeaderFunction->value] = $headerFunction;
|
||||
|
||||
// Set all options
|
||||
$handle->setOptions($options);
|
||||
|
||||
// Execute and stream directly to destination
|
||||
$handle->execute($destination);
|
||||
|
||||
// Parse response headers from buffer
|
||||
$statusCode = $handle->getInfo(\App\Framework\HttpClient\Curl\Info::ResponseCode);
|
||||
$status = \App\Framework\Http\Status::from((int) $statusCode);
|
||||
$headers = $this->responseParser->parseHeaders($headerBuffer);
|
||||
|
||||
// Get bytes written (if available)
|
||||
$bytesWritten = (int) ($handle->getInfo(\App\Framework\HttpClient\Curl\Info::SizeDownload) ?: 0);
|
||||
|
||||
return new StreamingResponse(
|
||||
status: $status,
|
||||
headers: $headers,
|
||||
bytesWritten: $bytesWritten
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send request with streaming body from source
|
||||
*
|
||||
* Streams HTTP request body from a readable stream resource.
|
||||
* Useful for large file uploads without loading entire file into memory.
|
||||
*
|
||||
* @param ClientRequest $request HTTP request to send (body will be replaced by stream)
|
||||
* @param resource $source Readable stream resource (e.g., fopen('file.txt', 'r'))
|
||||
* @param int|null $contentLength Content-Length in bytes (null for chunked transfer)
|
||||
* @return ClientResponse HTTP response
|
||||
* @throws CurlExecutionFailed If request execution fails
|
||||
*/
|
||||
public function sendStreamingUpload(ClientRequest $request, $source, ?int $contentLength = null): ClientResponse
|
||||
{
|
||||
if (! is_resource($source)) {
|
||||
throw new \InvalidArgumentException('Source must be a valid stream resource');
|
||||
}
|
||||
|
||||
$handle = new Handle();
|
||||
|
||||
try {
|
||||
// Build options using HandleOption enum
|
||||
$options = $this->requestBuilder->buildOptions($request);
|
||||
|
||||
// Remove CURLOPT_POSTFIELDS (we're using stream)
|
||||
unset($options[\App\Framework\HttpClient\Curl\HandleOption::PostFields->value]);
|
||||
|
||||
// Set streaming upload options
|
||||
$options[\App\Framework\HttpClient\Curl\HandleOption::Upload->value] = true;
|
||||
$options[\App\Framework\HttpClient\Curl\HandleOption::InFile->value] = $source;
|
||||
|
||||
if ($contentLength !== null) {
|
||||
$options[\App\Framework\HttpClient\Curl\HandleOption::InFileSize->value] = $contentLength;
|
||||
}
|
||||
|
||||
// Handle authentication
|
||||
if ($request->options->auth !== null) {
|
||||
$authResult = $this->authenticationHandler->configure($request->options->auth, $request->headers);
|
||||
|
||||
if ($authResult->headers !== $request->headers) {
|
||||
$updatedRequest = $request->with(['headers' => $authResult->headers]);
|
||||
$options = array_replace($options, $this->requestBuilder->buildOptions($updatedRequest));
|
||||
unset($options[\App\Framework\HttpClient\Curl\HandleOption::PostFields->value]);
|
||||
$options[\App\Framework\HttpClient\Curl\HandleOption::Upload->value] = true;
|
||||
$options[\App\Framework\HttpClient\Curl\HandleOption::InFile->value] = $source;
|
||||
if ($contentLength !== null) {
|
||||
$options[\App\Framework\HttpClient\Curl\HandleOption::InFileSize->value] = $contentLength;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($authResult->curlOptions)) {
|
||||
$options = array_replace($options, $authResult->curlOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Set all options
|
||||
$handle->setOptions($options);
|
||||
|
||||
// Execute request (automatically uses CURLOPT_RETURNTRANSFER)
|
||||
$rawResponse = $handle->fetch();
|
||||
|
||||
// Parse response
|
||||
return $this->responseParser->parse($rawResponse, $handle->getResource());
|
||||
} catch (\Throwable $e) {
|
||||
throw new CurlExecutionFailed($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -127,6 +269,7 @@ final readonly class CurlHttpClient implements HttpClient
|
||||
status: Status::from($status),
|
||||
headers: $headers,
|
||||
body: $body
|
||||
);*/
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user