diff options
author | Elizabeth Danzberger <lizzy7128@tutanota.de> | 2024-12-18 16:42:16 -0500 |
---|---|---|
committer | Elizabeth Danzberger <lizzy7128@tutanota.de> | 2025-01-15 16:38:18 -0500 |
commit | fdfeb7f265bfaef46bab8f3b506df6f69807d435 (patch) | |
tree | 64b158214c9a2edd341f9b0dbcb92b55a3a74042 /apps/files | |
parent | 6da58974a1a75275d99a75417ddd8f5d47851845 (diff) | |
download | nextcloud-server-fdfeb7f265bfaef46bab8f3b506df6f69807d435.tar.gz nextcloud-server-fdfeb7f265bfaef46bab8f3b506df6f69807d435.zip |
feat(api): File conversion API
Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>
Diffstat (limited to 'apps/files')
-rw-r--r-- | apps/files/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/files/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/files/lib/Capabilities.php | 9 | ||||
-rw-r--r-- | apps/files/lib/Controller/ConversionApiController.php | 95 | ||||
-rw-r--r-- | apps/files/openapi.json | 149 | ||||
-rw-r--r-- | apps/files/tests/Controller/ConversionApiControllerTest.php | 91 |
6 files changed, 345 insertions, 1 deletions
diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 7975c6f3d83..5922560f521 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -42,6 +42,7 @@ return array( 'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php', + 'OCA\\Files\\Controller\\ConversionApiController' => $baseDir . '/../lib/Controller/ConversionApiController.php', 'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php', 'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index f2cb858f313..bf489b037f7 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -57,6 +57,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php', + 'OCA\\Files\\Controller\\ConversionApiController' => __DIR__ . '/..' . '/../lib/Controller/ConversionApiController.php', 'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php', 'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php', diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index 16ea42eae22..88efb4fcaf0 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -10,18 +10,21 @@ namespace OCA\Files; use OC\Files\FilenameValidator; use OCA\Files\Service\ChunkedUploadConfig; use OCP\Capabilities\ICapability; +use OCP\Files\Conversion\ConversionMimeTuple; +use OCP\Files\Conversion\IConversionManager; class Capabilities implements ICapability { public function __construct( protected FilenameValidator $filenameValidator, + protected IConversionManager $fileConversionManager, ) { } /** * Return this classes capabilities * - * @return array{files: array{'$comment': ?string, bigfilechunking: bool, blacklisted_files: list<mixed>, forbidden_filenames: list<string>, forbidden_filename_basenames: list<string>, forbidden_filename_characters: list<string>, forbidden_filename_extensions: list<string>, chunked_upload: array{max_size: int, max_parallel_count: int}}} + * @return array{files: array{'$comment': ?string, bigfilechunking: bool, blacklisted_files: list<mixed>, forbidden_filenames: list<string>, forbidden_filename_basenames: list<string>, forbidden_filename_characters: list<string>, forbidden_filename_extensions: list<string>, chunked_upload: array{max_size: int, max_parallel_count: int}, file_conversions: list<array{from: string, to: list<array{mime: string, name: string}>}>}} */ public function getCapabilities(): array { return [ @@ -38,6 +41,10 @@ class Capabilities implements ICapability { 'max_size' => ChunkedUploadConfig::getMaxChunkSize(), 'max_parallel_count' => ChunkedUploadConfig::getMaxParallelCount(), ], + + 'file_conversions' => array_map(function (ConversionMimeTuple $mimeTuple) { + return $mimeTuple->jsonSerialize(); + }, $this->fileConversionManager->getMimeTypes()), ], ]; } diff --git a/apps/files/lib/Controller/ConversionApiController.php b/apps/files/lib/Controller/ConversionApiController.php new file mode 100644 index 00000000000..1a1b7da1787 --- /dev/null +++ b/apps/files/lib/Controller/ConversionApiController.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files\Controller; + +use OC\Files\Utils\PathHelper; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\UserRateLimit; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\Files\Conversion\IConversionManager; +use OCP\Files\File; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\IRequest; + +class ConversionApiController extends OCSController { + public function __construct( + string $appName, + IRequest $request, + private IConversionManager $fileConversionManager, + private IRootFolder $rootFolder, + private IL10N $l10n, + private ?string $userId, + ) { + parent::__construct($appName, $request); + } + + /** + * Converts a file from one MIME type to another + * + * @param int $fileId ID of the file to be converted + * @param string $targetMimeType The MIME type to which you want to convert the file + * @param string|null $destination The target path of the converted file. Written to a temporary file if left empty + * + * @return DataResponse<Http::STATUS_CREATED, array{path: string}, array{}> + * + * 201: File was converted and written to the destination or temporary file + * + * @throws OCSException The file was unable to be converted + * @throws OCSNotFoundException The file to be converted was not found + */ + #[NoAdminRequired] + #[UserRateLimit(limit: 25, period: 120)] + #[ApiRoute(verb: 'POST', url: '/api/v1/convert')] + public function convert(int $fileId, string $targetMimeType, ?string $destination = null): DataResponse { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $file = $userFolder->getFirstNodeById($fileId); + + if (!($file instanceof File)) { + throw new OCSNotFoundException($this->l10n->t('The file cannot be found')); + } + + if ($destination !== null) { + $destination = PathHelper::normalizePath($destination); + $parentDir = dirname($destination); + + if (!$userFolder->nodeExists($parentDir)) { + throw new OCSNotFoundException($this->l10n->t('The destination path does not exist: %1$s', [$parentDir])); + } + + if (!$userFolder->get($parentDir)->isCreatable()) { + throw new OCSForbiddenException(); + } + + $destination = $userFolder->getFullPath($destination); + } + + try { + $convertedFile = $this->fileConversionManager->convert($file, $targetMimeType, $destination); + } catch (\Exception $e) { + throw new OCSException($e->getMessage()); + } + + $convertedFileRelativePath = $userFolder->getRelativePath($convertedFile); + if ($convertedFileRelativePath === null) { + throw new OCSNotFoundException($this->l10n->t('Could not get relative path to converted file')); + } + + return new DataResponse([ + 'path' => $convertedFileRelativePath, + ], Http::STATUS_CREATED); + } +} diff --git a/apps/files/openapi.json b/apps/files/openapi.json index 2c33097438f..c196353cdd1 100644 --- a/apps/files/openapi.json +++ b/apps/files/openapi.json @@ -37,6 +37,7 @@ "forbidden_filename_characters", "forbidden_filename_extensions", "chunked_upload", + "file_conversions", "directEditing" ], "properties": { @@ -94,6 +95,27 @@ } } }, + "file_conversions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "type": "string" + }, + "to": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, "directEditing": { "type": "object", "required": [ @@ -2221,6 +2243,133 @@ } } } + }, + "/ocs/v2.php/apps/files/api/v1/convert": { + "post": { + "operationId": "conversion_api-convert", + "summary": "Converts a file from one MIME type to another", + "tags": [ + "conversion_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "fileId", + "targetMimeType" + ], + "properties": { + "fileId": { + "type": "integer", + "format": "int64", + "description": "ID of the file to be converted" + }, + "targetMimeType": { + "type": "string", + "description": "The MIME type to which you want to convert the file" + }, + "destination": { + "type": "string", + "nullable": true, + "description": "The target path of the converted file. Written to a temporary file if left empty" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "201": { + "description": "File was converted and written to the destination or temporary file", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "The file to be converted was not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } } }, "tags": [] diff --git a/apps/files/tests/Controller/ConversionApiControllerTest.php b/apps/files/tests/Controller/ConversionApiControllerTest.php new file mode 100644 index 00000000000..d2e68a8a32c --- /dev/null +++ b/apps/files/tests/Controller/ConversionApiControllerTest.php @@ -0,0 +1,91 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files\Controller; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\Files\Conversion\IConversionManager; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +/** + * Class ConversionApiController + * + * @package OCA\Files\Controller + */ +class ConversionApiControllerTest extends TestCase { + private string $appName = 'files'; + private ConversionApiController $conversionApiController; + private IRequest&MockObject $request; + private IConversionManager&MockObject $fileConversionManager; + private IRootFolder&MockObject $rootFolder; + private File&MockObject $file; + private Folder&MockObject $userFolder; + private IL10N&MockObject $l10n; + private string $user; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->fileConversionManager = $this->createMock(IConversionManager::class); + $this->file = $this->createMock(File::class); + $this->l10n = $this->createMock(IL10N::class); + $this->user = 'userid'; + + $this->userFolder = $this->createMock(Folder::class); + + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->rootFolder->method('getUserFolder')->with($this->user)->willReturn($this->userFolder); + + $this->conversionApiController = new ConversionApiController( + $this->appName, + $this->request, + $this->fileConversionManager, + $this->rootFolder, + $this->l10n, + $this->user, + ); + } + + public function testThrowsNotFoundException() { + $this->expectException(OCSNotFoundException::class); + $this->conversionApiController->convert(42, 'image/png'); + } + + public function testThrowsOcsException() { + $this->userFolder->method('getFirstNodeById')->with(42)->willReturn($this->file); + $this->fileConversionManager->method('convert')->willThrowException(new \Exception()); + + $this->expectException(OCSException::class); + $this->conversionApiController->convert(42, 'image/png'); + } + + public function testConvert() { + $convertedFileAbsolutePath = $this->user . '/files/test.png'; + + $this->userFolder->method('getFirstNodeById')->with(42)->willReturn($this->file); + $this->userFolder->method('getRelativePath')->with($convertedFileAbsolutePath)->willReturn('/test.png'); + + $this->fileConversionManager->method('convert')->with($this->file, 'image/png', null)->willReturn($convertedFileAbsolutePath); + + $actual = $this->conversionApiController->convert(42, 'image/png', null); + $expected = new DataResponse([ + 'path' => '/test.png', + ], Http::STATUS_CREATED); + + $this->assertEquals($expected, $actual); + } +} |