aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files
diff options
context:
space:
mode:
authorElizabeth Danzberger <lizzy7128@tutanota.de>2024-12-18 16:42:16 -0500
committerElizabeth Danzberger <lizzy7128@tutanota.de>2025-01-15 16:38:18 -0500
commitfdfeb7f265bfaef46bab8f3b506df6f69807d435 (patch)
tree64b158214c9a2edd341f9b0dbcb92b55a3a74042 /apps/files
parent6da58974a1a75275d99a75417ddd8f5d47851845 (diff)
downloadnextcloud-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.php1
-rw-r--r--apps/files/composer/composer/autoload_static.php1
-rw-r--r--apps/files/lib/Capabilities.php9
-rw-r--r--apps/files/lib/Controller/ConversionApiController.php95
-rw-r--r--apps/files/openapi.json149
-rw-r--r--apps/files/tests/Controller/ConversionApiControllerTest.php91
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);
+ }
+}