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:
2025-11-04 20:39:48 +01:00
parent 700fe8118b
commit 3ed2685e74
80 changed files with 9891 additions and 850 deletions

View File

@@ -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
);*/
);
}
*/
}