From b5d61410f7bffa701845311a7bf5f59b1d7634bd Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Tue, 18 Aug 2026 11:03:43 -0400 Subject: [PATCH] fix: combine StreamGeneratorResponse with StreamTraversableResponse Signed-off-by: SebastianKrupinski --- .../Controller/CalendarExportController.php | 6 +- .../Controller/CalendarImportController.php | 8 +-- .../Controller/ContactsImportController.php | 8 +-- apps/dav/openapi.json | 15 +++-- lib/composer/composer/autoload_classmap.php | 1 - lib/composer/composer/autoload_static.php | 1 - .../Http/StreamGeneratorResponse.php | 63 ------------------- openapi.json | 15 +++-- ....php => StreamTraversableResponseTest.php} | 13 ++-- 9 files changed, 40 insertions(+), 90 deletions(-) delete mode 100644 lib/public/AppFramework/Http/StreamGeneratorResponse.php rename tests/lib/AppFramework/Http/{StreamGeneratorResponseTest.php => StreamTraversableResponseTest.php} (63%) diff --git a/apps/dav/lib/Controller/CalendarExportController.php b/apps/dav/lib/Controller/CalendarExportController.php index 0acacbe1f147b..cd66c139f89ed 100644 --- a/apps/dav/lib/Controller/CalendarExportController.php +++ b/apps/dav/lib/Controller/CalendarExportController.php @@ -15,7 +15,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\StreamGeneratorResponse; +use OCP\AppFramework\Http\StreamTraversableResponse; use OCP\AppFramework\OCSController; use OCP\Calendar\CalendarExportOptions; use OCP\Calendar\ICalendarExport; @@ -46,7 +46,7 @@ public function __construct( * @param array{rangeStart:string,rangeCount:positive-int} $options configuration options * @param string|null $user system user id * - * @return StreamGeneratorResponse | DataResponse + * @return StreamTraversableResponse | DataResponse * * 200: data in requested format * 400: invalid parameters @@ -99,7 +99,7 @@ public function export(string $target, ?string $type = null, ?array $options = n 'xcal' => 'application/calendar+xml; charset=UTF-8', default => 'text/calendar; charset=UTF-8' }; - $response = new StreamGeneratorResponse($this->exportService->export($calendar, $options), $contentType, Http::STATUS_OK); + $response = new StreamTraversableResponse($this->exportService->export($calendar, $options), Http::STATUS_OK, ['Content-Type' => $contentType]); $response->cacheFor(0); return $response; diff --git a/apps/dav/lib/Controller/CalendarImportController.php b/apps/dav/lib/Controller/CalendarImportController.php index b840ebc77a0cb..83c40fa5651b4 100644 --- a/apps/dav/lib/Controller/CalendarImportController.php +++ b/apps/dav/lib/Controller/CalendarImportController.php @@ -20,7 +20,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\StreamGeneratorResponse; +use OCP\AppFramework\Http\StreamTraversableResponse; use OCP\AppFramework\OCSController; use OCP\Calendar\CalendarImportOptions; use OCP\Calendar\IManager; @@ -53,7 +53,7 @@ public function __construct( * @param string $data calendar data * @param string|null $user system user id * - * @return StreamGeneratorResponse | DataResponse + * @return StreamTraversableResponse | DataResponse * * 200: NDJSON stream of import event objects * 400: invalid parameters @@ -62,7 +62,7 @@ public function __construct( #[ApiRoute(verb: 'POST', url: '/import', root: '/calendar')] #[UserRateLimit(limit: 10, period: 3600)] #[NoAdminRequired] - public function import(string $transaction, string $target, array $options, string $data, ?string $user = null): DataResponse|StreamGeneratorResponse { + public function import(string $transaction, string $target, array $options, string $data, ?string $user = null): DataResponse|StreamTraversableResponse { $calendarId = $target; $format = isset($options['format']) ? $options['format'] : null; $validation = isset($options['validation']) ? (int)$options['validation'] : null; @@ -144,6 +144,6 @@ public function import(string $transaction, string $target, array $options, stri } })(); - return new StreamGeneratorResponse($stream, 'application/x-ndjson'); + return new StreamTraversableResponse($stream, Http::STATUS_OK, ['Content-Type' => 'application/x-ndjson']); } } diff --git a/apps/dav/lib/Controller/ContactsImportController.php b/apps/dav/lib/Controller/ContactsImportController.php index 177639622a0c0..fe1264a5f3b1d 100644 --- a/apps/dav/lib/Controller/ContactsImportController.php +++ b/apps/dav/lib/Controller/ContactsImportController.php @@ -19,7 +19,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\StreamGeneratorResponse; +use OCP\AppFramework\Http\StreamTraversableResponse; use OCP\AppFramework\OCSController; use OCP\Contacts\ContactsImportOptions; use OCP\Contacts\IManager; @@ -53,7 +53,7 @@ public function __construct( * @param string $data contacts data * @param string|null $user system user id * - * @return StreamGeneratorResponse | DataResponse + * @return StreamTraversableResponse | DataResponse * * 200: NDJSON stream of import event objects * 400: invalid parameters @@ -62,7 +62,7 @@ public function __construct( #[ApiRoute(verb: 'POST', url: '/import', root: '/contacts')] #[UserRateLimit(limit: 10, period: 3600)] #[NoAdminRequired] - public function import(string $transaction, string $target, array $options, string $data, ?string $user = null): DataResponse|StreamGeneratorResponse { + public function import(string $transaction, string $target, array $options, string $data, ?string $user = null): DataResponse|StreamTraversableResponse { $addressBookId = $target; $format = isset($options['format']) ? $options['format'] : null; $validation = isset($options['validation']) ? (int)$options['validation'] : null; @@ -141,6 +141,6 @@ public function import(string $transaction, string $target, array $options, stri } })(); - return new StreamGeneratorResponse($stream, 'application/x-ndjson'); + return new StreamTraversableResponse($stream, Http::STATUS_OK, ['Content-Type' => 'application/x-ndjson']); } } diff --git a/apps/dav/openapi.json b/apps/dav/openapi.json index dad136c0472e0..f8346e47d5fdf 100644 --- a/apps/dav/openapi.json +++ b/apps/dav/openapi.json @@ -1660,17 +1660,20 @@ "content": { "text/calendar; charset=UTF-8": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } }, "application/calendar+json; charset=UTF-8": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } }, "application/calendar+xml; charset=UTF-8": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } } } @@ -1885,7 +1888,8 @@ "content": { "application/x-ndjson": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } } } @@ -2088,7 +2092,8 @@ "content": { "application/x-ndjson": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index d2e90e1c93050..5c01b0c2221ed 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -166,7 +166,6 @@ 'OCP\\AppFramework\\Http\\RedirectToDefaultAppResponse' => $baseDir . '/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php', 'OCP\\AppFramework\\Http\\Response' => $baseDir . '/lib/public/AppFramework/Http/Response.php', 'OCP\\AppFramework\\Http\\StandaloneTemplateResponse' => $baseDir . '/lib/public/AppFramework/Http/StandaloneTemplateResponse.php', - 'OCP\\AppFramework\\Http\\StreamGeneratorResponse' => $baseDir . '/lib/public/AppFramework/Http/StreamGeneratorResponse.php', 'OCP\\AppFramework\\Http\\StreamResponse' => $baseDir . '/lib/public/AppFramework/Http/StreamResponse.php', 'OCP\\AppFramework\\Http\\StreamTraversableResponse' => $baseDir . '/lib/public/AppFramework/Http/StreamTraversableResponse.php', 'OCP\\AppFramework\\Http\\TemplateResponse' => $baseDir . '/lib/public/AppFramework/Http/TemplateResponse.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1a81914da4244..669c234843b58 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -207,7 +207,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\AppFramework\\Http\\RedirectToDefaultAppResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php', 'OCP\\AppFramework\\Http\\Response' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Response.php', 'OCP\\AppFramework\\Http\\StandaloneTemplateResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StandaloneTemplateResponse.php', - 'OCP\\AppFramework\\Http\\StreamGeneratorResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StreamGeneratorResponse.php', 'OCP\\AppFramework\\Http\\StreamResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StreamResponse.php', 'OCP\\AppFramework\\Http\\StreamTraversableResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StreamTraversableResponse.php', 'OCP\\AppFramework\\Http\\TemplateResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/TemplateResponse.php', diff --git a/lib/public/AppFramework/Http/StreamGeneratorResponse.php b/lib/public/AppFramework/Http/StreamGeneratorResponse.php deleted file mode 100644 index 16a7c582aa309..0000000000000 --- a/lib/public/AppFramework/Http/StreamGeneratorResponse.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @template-extends Response> - */ -class StreamGeneratorResponse extends Response implements ICallbackResponse { - protected $generator; - - /** - * @since 35.0.0 - * - * @param Generator $generator the function to call to generate the response - * @param string $contentType http response content type e.g. 'application/json; charset=UTF-8' - * @param S $status http response status - * @param array|null $headers additional headers - */ - public function __construct(Generator $generator, string $contentType, int $status = Http::STATUS_OK, ?array $headers = []) { - parent::__construct(); - - $this->generator = $generator; - - $this->setStatus($status); - $this->addHeader('Content-Type', $contentType); - - foreach ($headers as $key => $value) { - $this->addHeader($key, $value); - } - } - - /** - * Streams content directly to client - * - * @since 35.0.0 - * - * @param IOutput $output a small wrapper that handles output - */ - #[\Override] - public function callback(IOutput $output): void { - - foreach ($this->generator as $chunk) { - print($chunk); - flush(); - } - - } - -} diff --git a/openapi.json b/openapi.json index 69e2ca0b52822..9d2384c6b63d4 100644 --- a/openapi.json +++ b/openapi.json @@ -21661,17 +21661,20 @@ "content": { "text/calendar; charset=UTF-8": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } }, "application/calendar+json; charset=UTF-8": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } }, "application/calendar+xml; charset=UTF-8": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } } } @@ -21886,7 +21889,8 @@ "content": { "application/x-ndjson": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } } } @@ -22089,7 +22093,8 @@ "content": { "application/x-ndjson": { "schema": { - "anyOf": [] + "type": "string", + "format": "binary" } } } diff --git a/tests/lib/AppFramework/Http/StreamGeneratorResponseTest.php b/tests/lib/AppFramework/Http/StreamTraversableResponseTest.php similarity index 63% rename from tests/lib/AppFramework/Http/StreamGeneratorResponseTest.php rename to tests/lib/AppFramework/Http/StreamTraversableResponseTest.php index 624d1ded8a271..81c6001183e2a 100644 --- a/tests/lib/AppFramework/Http/StreamGeneratorResponseTest.php +++ b/tests/lib/AppFramework/Http/StreamTraversableResponseTest.php @@ -9,9 +9,9 @@ namespace Test\AppFramework\Http; use OCP\AppFramework\Http\IOutput; -use OCP\AppFramework\Http\StreamGeneratorResponse; +use OCP\AppFramework\Http\StreamTraversableResponse; -class StreamGeneratorResponseTest extends \Test\TestCase { +class StreamTraversableResponseTest extends \Test\TestCase { protected function setUp(): void { parent::setUp(); @@ -22,7 +22,7 @@ public function testConstructor() { yield 'chunk1'; yield 'chunk2'; }; - $response = new StreamGeneratorResponse($generator(), 'text/plain'); + $response = new StreamTraversableResponse($generator(), 200, ['Content-Type' => 'text/plain']); $headers = $response->getHeaders(); $this->assertEquals('text/plain', $headers['Content-Type']); @@ -37,8 +37,13 @@ public function testCallback() { $count++; yield 'chunk2'; }; - $response = new StreamGeneratorResponse($generator(), 'text/plain'); + $response = new StreamTraversableResponse($generator(), 200, ['Content-Type' => 'text/plain']); $output = $this->createMock(IOutput::class); + $output->expects($this->exactly(2)) + ->method('setOutput') + ->with($this->callback(function ($chunk) { + return in_array($chunk, ['chunk1', 'chunk2'], true); + })); $response->callback($output); $this->assertEquals($count, 2);