aboutsummaryrefslogtreecommitdiffstats
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
parent6da58974a1a75275d99a75417ddd8f5d47851845 (diff)
downloadnextcloud-server-fdfeb7f265bfaef46bab8f3b506df6f69807d435.tar.gz
nextcloud-server-fdfeb7f265bfaef46bab8f3b506df6f69807d435.zip
feat(api): File conversion API
Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>
-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
-rw-r--r--apps/testing/composer/composer/autoload_classmap.php1
-rw-r--r--apps/testing/composer/composer/autoload_static.php1
-rw-r--r--apps/testing/lib/AppInfo/Application.php3
-rw-r--r--apps/testing/lib/Conversion/ConversionProvider.php40
-rw-r--r--config/config.sample.php8
-rw-r--r--lib/composer/composer/autoload_classmap.php4
-rw-r--r--lib/composer/composer/autoload_static.php4
-rw-r--r--lib/private/AppFramework/Bootstrap/RegistrationContext.php25
-rw-r--r--lib/private/CapabilitiesManager.php2
-rw-r--r--lib/private/Files/Conversion/ConversionManager.php152
-rw-r--r--lib/private/Server.php4
-rw-r--r--lib/public/AppFramework/Bootstrap/IRegistrationContext.php13
-rw-r--r--lib/public/Files/Conversion/ConversionMimeTuple.php44
-rw-r--r--lib/public/Files/Conversion/IConversionManager.php46
-rw-r--r--lib/public/Files/Conversion/IConversionProvider.php41
21 files changed, 732 insertions, 2 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);
+ }
+}
diff --git a/apps/testing/composer/composer/autoload_classmap.php b/apps/testing/composer/composer/autoload_classmap.php
index 83d1fc771fc..e7f1ce74466 100644
--- a/apps/testing/composer/composer/autoload_classmap.php
+++ b/apps/testing/composer/composer/autoload_classmap.php
@@ -12,6 +12,7 @@ return array(
'OCA\\Testing\\Controller\\ConfigController' => $baseDir . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php',
'OCA\\Testing\\Controller\\RateLimitTestController' => $baseDir . '/../lib/Controller/RateLimitTestController.php',
+ 'OCA\\Testing\\Conversion\\ConversionProvider' => $baseDir . '/../lib/Conversion/ConversionProvider.php',
'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/GetDeclarativeSettingsValueListener.php',
'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => $baseDir . '/../lib/Listener/RegisterDeclarativeSettingsListener.php',
'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/SetDeclarativeSettingsValueListener.php',
diff --git a/apps/testing/composer/composer/autoload_static.php b/apps/testing/composer/composer/autoload_static.php
index 3dc4bfe2fd6..f87a822aaf2 100644
--- a/apps/testing/composer/composer/autoload_static.php
+++ b/apps/testing/composer/composer/autoload_static.php
@@ -27,6 +27,7 @@ class ComposerStaticInitTesting
'OCA\\Testing\\Controller\\ConfigController' => __DIR__ . '/..' . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php',
'OCA\\Testing\\Controller\\RateLimitTestController' => __DIR__ . '/..' . '/../lib/Controller/RateLimitTestController.php',
+ 'OCA\\Testing\\Conversion\\ConversionProvider' => __DIR__ . '/..' . '/../lib/Conversion/ConversionProvider.php',
'OCA\\Testing\\Listener\\GetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/GetDeclarativeSettingsValueListener.php',
'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => __DIR__ . '/..' . '/../lib/Listener/RegisterDeclarativeSettingsListener.php',
'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/SetDeclarativeSettingsValueListener.php',
diff --git a/apps/testing/lib/AppInfo/Application.php b/apps/testing/lib/AppInfo/Application.php
index 3502b78402e..bbd9e288cc1 100644
--- a/apps/testing/lib/AppInfo/Application.php
+++ b/apps/testing/lib/AppInfo/Application.php
@@ -7,6 +7,7 @@
namespace OCA\Testing\AppInfo;
use OCA\Testing\AlternativeHomeUserBackend;
+use OCA\Testing\Conversion\ConversionProvider;
use OCA\Testing\Listener\GetDeclarativeSettingsValueListener;
use OCA\Testing\Listener\RegisterDeclarativeSettingsListener;
use OCA\Testing\Listener\SetDeclarativeSettingsValueListener;
@@ -49,6 +50,8 @@ class Application extends App implements IBootstrap {
$context->registerTaskProcessingProvider(FakeTranscribeProvider::class);
$context->registerTaskProcessingProvider(FakeContextWriteProvider::class);
+ $context->registerFileConversionProvider(ConversionProvider::class);
+
$context->registerDeclarativeSettings(DeclarativeSettingsForm::class);
$context->registerEventListener(DeclarativeSettingsRegisterFormEvent::class, RegisterDeclarativeSettingsListener::class);
$context->registerEventListener(DeclarativeSettingsGetValueEvent::class, GetDeclarativeSettingsValueListener::class);
diff --git a/apps/testing/lib/Conversion/ConversionProvider.php b/apps/testing/lib/Conversion/ConversionProvider.php
new file mode 100644
index 00000000000..15e468b0756
--- /dev/null
+++ b/apps/testing/lib/Conversion/ConversionProvider.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Testing\Conversion;
+
+use OCP\Files\Conversion\ConversionMimeTuple;
+use OCP\Files\Conversion\IConversionProvider;
+use OCP\Files\File;
+use OCP\IL10N;
+
+class ConversionProvider implements IConversionProvider {
+ public function __construct(
+ private IL10N $l10n,
+ ) {
+ }
+
+ public function getSupportedMimeTypes(): array {
+ return [
+ new ConversionMimeTuple('image/jpeg', [
+ ['mime' => 'image/png', 'name' => $this->l10n->t('Image (.png)')],
+ ])
+ ];
+ }
+
+ public function convertFile(File $file, string $targetMimeType): mixed {
+ $image = imagecreatefromstring($file->getContent());
+
+ imagepalettetotruecolor($image);
+
+ ob_start();
+ imagepng($image);
+ return ob_get_clean();
+ }
+}
diff --git a/config/config.sample.php b/config/config.sample.php
index ebc8427558f..b0b0f21853b 100644
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -1429,6 +1429,14 @@ $CONFIG = [
'metadata_max_filesize' => 256,
/**
+ * Maximum file size for file conversion.
+ * If a file exceeds this size, the file will not be converted.
+ *
+ * Default: 100 MiB
+ */
+'max_file_conversion_filesize' => 100,
+
+/**
* LDAP
*
* Global settings used by LDAP User and Group Backend
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 9809ea53508..d6a8ac73c5f 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -377,6 +377,9 @@ return array(
'OCP\\Files\\Config\\IRootMountProvider' => $baseDir . '/lib/public/Files/Config/IRootMountProvider.php',
'OCP\\Files\\Config\\IUserMountCache' => $baseDir . '/lib/public/Files/Config/IUserMountCache.php',
'OCP\\Files\\ConnectionLostException' => $baseDir . '/lib/public/Files/ConnectionLostException.php',
+ 'OCP\\Files\\Conversion\\ConversionMimeTuple' => $baseDir . '/lib/public/Files/Conversion/ConversionMimeTuple.php',
+ 'OCP\\Files\\Conversion\\IConversionManager' => $baseDir . '/lib/public/Files/Conversion/IConversionManager.php',
+ 'OCP\\Files\\Conversion\\IConversionProvider' => $baseDir . '/lib/public/Files/Conversion/IConversionProvider.php',
'OCP\\Files\\DavUtil' => $baseDir . '/lib/public/Files/DavUtil.php',
'OCP\\Files\\EmptyFileNameException' => $baseDir . '/lib/public/Files/EmptyFileNameException.php',
'OCP\\Files\\EntityTooLargeException' => $baseDir . '/lib/public/Files/EntityTooLargeException.php',
@@ -1575,6 +1578,7 @@ return array(
'OC\\Files\\Config\\MountProviderCollection' => $baseDir . '/lib/private/Files/Config/MountProviderCollection.php',
'OC\\Files\\Config\\UserMountCache' => $baseDir . '/lib/private/Files/Config/UserMountCache.php',
'OC\\Files\\Config\\UserMountCacheListener' => $baseDir . '/lib/private/Files/Config/UserMountCacheListener.php',
+ 'OC\\Files\\Conversion\\ConversionManager' => $baseDir . '/lib/private/Files/Conversion/ConversionManager.php',
'OC\\Files\\FileInfo' => $baseDir . '/lib/private/Files/FileInfo.php',
'OC\\Files\\FilenameValidator' => $baseDir . '/lib/private/Files/FilenameValidator.php',
'OC\\Files\\Filesystem' => $baseDir . '/lib/private/Files/Filesystem.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index f612e0d5fc6..0fba81fd3cd 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -418,6 +418,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Files\\Config\\IRootMountProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IRootMountProvider.php',
'OCP\\Files\\Config\\IUserMountCache' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IUserMountCache.php',
'OCP\\Files\\ConnectionLostException' => __DIR__ . '/../../..' . '/lib/public/Files/ConnectionLostException.php',
+ 'OCP\\Files\\Conversion\\ConversionMimeTuple' => __DIR__ . '/../../..' . '/lib/public/Files/Conversion/ConversionMimeTuple.php',
+ 'OCP\\Files\\Conversion\\IConversionManager' => __DIR__ . '/../../..' . '/lib/public/Files/Conversion/IConversionManager.php',
+ 'OCP\\Files\\Conversion\\IConversionProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Conversion/IConversionProvider.php',
'OCP\\Files\\DavUtil' => __DIR__ . '/../../..' . '/lib/public/Files/DavUtil.php',
'OCP\\Files\\EmptyFileNameException' => __DIR__ . '/../../..' . '/lib/public/Files/EmptyFileNameException.php',
'OCP\\Files\\EntityTooLargeException' => __DIR__ . '/../../..' . '/lib/public/Files/EntityTooLargeException.php',
@@ -1616,6 +1619,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Files\\Config\\MountProviderCollection' => __DIR__ . '/../../..' . '/lib/private/Files/Config/MountProviderCollection.php',
'OC\\Files\\Config\\UserMountCache' => __DIR__ . '/../../..' . '/lib/private/Files/Config/UserMountCache.php',
'OC\\Files\\Config\\UserMountCacheListener' => __DIR__ . '/../../..' . '/lib/private/Files/Config/UserMountCacheListener.php',
+ 'OC\\Files\\Conversion\\ConversionManager' => __DIR__ . '/../../..' . '/lib/private/Files/Conversion/ConversionManager.php',
'OC\\Files\\FileInfo' => __DIR__ . '/../../..' . '/lib/private/Files/FileInfo.php',
'OC\\Files\\FilenameValidator' => __DIR__ . '/../../..' . '/lib/private/Files/FilenameValidator.php',
'OC\\Files\\Filesystem' => __DIR__ . '/../../..' . '/lib/private/Files/Filesystem.php',
diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
index 77cc58c6468..c3b829825c2 100644
--- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php
+++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
@@ -154,6 +154,9 @@ class RegistrationContext {
/** @var ServiceRegistration<\OCP\TaskProcessing\ITaskType>[] */
private array $taskProcessingTaskTypes = [];
+
+ /** @var ServiceRegistration<\OCP\Files\Conversion\IConversionProvider>[] */
+ private array $fileConversionProviders = [];
/** @var ServiceRegistration<IMailProvider>[] */
private $mailProviders = [];
@@ -421,6 +424,13 @@ class RegistrationContext {
);
}
+ public function registerFileConversionProvider(string $class): void {
+ $this->context->registerFileConversionProvider(
+ $this->appId,
+ $class
+ );
+ }
+
public function registerMailProvider(string $class): void {
$this->context->registerMailProvider(
$this->appId,
@@ -626,6 +636,14 @@ class RegistrationContext {
public function registerTaskProcessingTaskType(string $appId, string $taskProcessingTaskTypeClass) {
$this->taskProcessingTaskTypes[] = new ServiceRegistration($appId, $taskProcessingTaskTypeClass);
}
+
+ /**
+ * @psalm-param class-string<\OCP\Files\Conversion\IConversionProvider> $class
+ */
+ public function registerFileConversionProvider(string $appId, string $class): void {
+ $this->fileConversionProviders[] = new ServiceRegistration($appId, $class);
+ }
+
/**
* @psalm-param class-string<IMailProvider> $migratorClass
*/
@@ -986,6 +1004,13 @@ class RegistrationContext {
}
/**
+ * @return ServiceRegistration<\OCP\Files\Conversion\IConversionProvider>[]
+ */
+ public function getFileConversionProviders(): array {
+ return $this->fileConversionProviders;
+ }
+
+ /**
* @return ServiceRegistration<IMailProvider>[]
*/
public function getMailProviders(): array {
diff --git a/lib/private/CapabilitiesManager.php b/lib/private/CapabilitiesManager.php
index d7bf25f078a..07076d9aacd 100644
--- a/lib/private/CapabilitiesManager.php
+++ b/lib/private/CapabilitiesManager.php
@@ -32,7 +32,7 @@ class CapabilitiesManager {
}
/**
- * Get an array of al the capabilities that are registered at this manager
+ * Get an array of all the capabilities that are registered at this manager
*
* @param bool $public get public capabilities only
* @throws \InvalidArgumentException
diff --git a/lib/private/Files/Conversion/ConversionManager.php b/lib/private/Files/Conversion/ConversionManager.php
new file mode 100644
index 00000000000..37fc9a6c754
--- /dev/null
+++ b/lib/private/Files/Conversion/ConversionManager.php
@@ -0,0 +1,152 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Files\Conversion;
+
+use OC\AppFramework\Bootstrap\Coordinator;
+use OC\SystemConfig;
+use OCP\Files\Conversion\ConversionMimeTuple;
+use OCP\Files\Conversion\IConversionManager;
+use OCP\Files\Conversion\IConversionProvider;
+use OCP\Files\File;
+use OCP\Files\GenericFileException;
+use OCP\Files\IRootFolder;
+use OCP\ITempManager;
+use OCP\PreConditionNotMetException;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+use Throwable;
+
+class ConversionManager implements IConversionManager {
+ /** @var string[] */
+ private array $preferredApps = [
+ 'richdocuments',
+ ];
+
+ /** @var IConversionProvider[] */
+ private array $preferredProviders = [];
+
+ /** @var IConversionProvider[] */
+ private array $providers = [];
+
+ public function __construct(
+ private Coordinator $coordinator,
+ private ContainerInterface $serverContainer,
+ private IRootFolder $rootFolder,
+ private ITempManager $tempManager,
+ private LoggerInterface $logger,
+ private SystemConfig $config,
+ ) {
+ }
+
+ public function hasProviders(): bool {
+ $context = $this->coordinator->getRegistrationContext();
+ return !empty($context->getFileConversionProviders());
+ }
+
+ public function getMimeTypes(): array {
+ $mimeTypes = [];
+
+ foreach ($this->getProviders() as $provider) {
+ $mimeTypes[] = $provider->getSupportedMimetypes();
+ }
+
+ /** @var list<ConversionMimeTuple> */
+ $mimeTypes = array_merge(...$mimeTypes);
+ return $mimeTypes;
+ }
+
+ public function convert(File $file, string $targetMimeType, ?string $destination = null): string {
+ if (!$this->hasProviders()) {
+ throw new PreConditionNotMetException('No file conversion providers available');
+ }
+
+ // Operate in mebibytes
+ $fileSize = $file->getSize() / (1024 * 1024);
+ $threshold = $this->config->getValue('max_file_conversion_filesize', 100);
+ if ($fileSize > $threshold) {
+ throw new GenericFileException('File is too large to convert');
+ }
+
+ $fileMimeType = $file->getMimetype();
+ $validProvider = $this->getValidProvider($fileMimeType, $targetMimeType);
+
+ if ($validProvider !== null) {
+ $convertedFile = $validProvider->convertFile($file, $targetMimeType);
+
+ if ($destination !== null) {
+ $convertedFile = $this->writeToDestination($destination, $convertedFile);
+ return $convertedFile->getPath();
+ }
+
+ $tmp = $this->tempManager->getTemporaryFile();
+ file_put_contents($tmp, $convertedFile);
+
+ return $tmp;
+ }
+
+ throw new RuntimeException('Could not convert file');
+ }
+
+ public function getProviders(): array {
+ if (count($this->providers) > 0) {
+ return $this->providers;
+ }
+
+ $context = $this->coordinator->getRegistrationContext();
+ foreach ($context->getFileConversionProviders() as $providerRegistration) {
+ $class = $providerRegistration->getService();
+ $appId = $providerRegistration->getAppId();
+
+ try {
+ if (in_array($appId, $this->preferredApps)) {
+ $this->preferredProviders[$class] = $this->serverContainer->get($class);
+ continue;
+ }
+
+ $this->providers[$class] = $this->serverContainer->get($class);
+ } catch (NotFoundExceptionInterface|ContainerExceptionInterface|Throwable $e) {
+ $this->logger->error('Failed to load file conversion provider ' . $class, [
+ 'exception' => $e,
+ ]);
+ }
+ }
+
+ return array_merge([], $this->preferredProviders, $this->providers);
+ }
+
+ private function writeToDestination(string $destination, mixed $content): File {
+ return $this->rootFolder->newFile($destination, $content);
+ }
+
+ private function getValidProvider(string $fileMimeType, string $targetMimeType): ?IConversionProvider {
+ $validProvider = null;
+ foreach ($this->getProviders() as $provider) {
+ $suitableMimeTypes = array_filter(
+ $provider->getSupportedMimeTypes(),
+ function (ConversionMimeTuple $mimeTuple) use ($fileMimeType, $targetMimeType) {
+ ['from' => $from, 'to' => $to] = $mimeTuple->jsonSerialize();
+
+ $supportsTargetMimeType = in_array($targetMimeType, array_column($to, 'mime'));
+ return ($from === $fileMimeType) && $supportsTargetMimeType;
+ }
+ );
+
+ if (!empty($suitableMimeTypes)) {
+ $validProvider = $provider;
+ break;
+ }
+ }
+
+ return $validProvider;
+ }
+}
diff --git a/lib/private/Server.php b/lib/private/Server.php
index a20c37732a7..f806c368eaf 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -45,6 +45,7 @@ use OC\Files\Cache\FileAccess;
use OC\Files\Config\MountProviderCollection;
use OC\Files\Config\UserMountCache;
use OC\Files\Config\UserMountCacheListener;
+use OC\Files\Conversion\ConversionManager;
use OC\Files\Lock\LockManager;
use OC\Files\Mount\CacheMountProvider;
use OC\Files\Mount\LocalHomeMountProvider;
@@ -155,6 +156,7 @@ use OCP\Federation\ICloudIdManager;
use OCP\Files\Cache\IFileAccess;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IUserMountCache;
+use OCP\Files\Conversion\IConversionManager;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\IRootFolder;
@@ -1258,6 +1260,8 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(ITranslationManager::class, TranslationManager::class);
+ $this->registerAlias(IConversionManager::class, ConversionManager::class);
+
$this->registerAlias(ISpeechToTextManager::class, SpeechToTextManager::class);
$this->registerAlias(IEventSourceFactory::class, EventSourceFactory::class);
diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
index 8a18ec8ae9d..011d1520414 100644
--- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
+++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
@@ -415,6 +415,19 @@ interface IRegistrationContext {
public function registerTaskProcessingTaskType(string $taskProcessingTaskTypeClass): void;
/**
+ * Register an implementation of \OCP\Files\Conversion\IConversionProvider
+ * that will handle the conversion of files from one MIME type to another
+ *
+ * @param string $class
+ * @psalm-param class-string<\OCP\Files\Conversion\IConversionProvider> $class
+ *
+ * @return void
+ *
+ * @since 31.0.0
+ */
+ public function registerFileConversionProvider(string $class): void;
+
+ /**
* Register a mail provider
*
* @param string $class
diff --git a/lib/public/Files/Conversion/ConversionMimeTuple.php b/lib/public/Files/Conversion/ConversionMimeTuple.php
new file mode 100644
index 00000000000..0180f3311f3
--- /dev/null
+++ b/lib/public/Files/Conversion/ConversionMimeTuple.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\Files\Conversion;
+
+use JsonSerializable;
+
+/**
+ * A tuple-like object representing both an original and target
+ * MIME type for a file conversion
+ *
+ * @since 31.0.0
+ */
+class ConversionMimeTuple implements JsonSerializable {
+ /**
+ * @param string $from The original MIME type of a file
+ * @param list<array{mime: string, name: string}> $to The desired MIME type for the file mapped to its translated name
+ *
+ * @since 31.0.0
+ */
+ public function __construct(
+ private string $from,
+ private array $to,
+ ) {
+ }
+
+ /**
+ * @return array{from: string, to: list<array{mime: string, name: string}>}
+ *
+ * @since 31.0.0
+ */
+ public function jsonSerialize(): array {
+ return [
+ 'from' => $this->from,
+ 'to' => $this->to,
+ ];
+ }
+}
diff --git a/lib/public/Files/Conversion/IConversionManager.php b/lib/public/Files/Conversion/IConversionManager.php
new file mode 100644
index 00000000000..59ff580fdf1
--- /dev/null
+++ b/lib/public/Files/Conversion/IConversionManager.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\Files\Conversion;
+
+use OCP\Files\File;
+
+/**
+ * @since 31.0.0
+ */
+interface IConversionManager {
+ /**
+ * Determines whether or not conversion providers are available
+ *
+ * @since 31.0.0
+ */
+ public function hasProviders(): bool;
+
+ /**
+ * Gets all supported MIME type conversions
+ *
+ * @return list<ConversionMimeTuple>
+ *
+ * @since 31.0.0
+ */
+ public function getMimeTypes(): array;
+
+ /**
+ * Convert a file to a given MIME type
+ *
+ * @param File $file The file to be converted
+ * @param string $targetMimeType The MIME type to convert the file to
+ * @param ?string $destination The destination to save the converted file
+ *
+ * @return string Path to the converted file
+ *
+ * @since 31.0.0
+ */
+ public function convert(File $file, string $targetMimeType, ?string $destination = null): string;
+}
diff --git a/lib/public/Files/Conversion/IConversionProvider.php b/lib/public/Files/Conversion/IConversionProvider.php
new file mode 100644
index 00000000000..b0a09fc93a4
--- /dev/null
+++ b/lib/public/Files/Conversion/IConversionProvider.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\Files\Conversion;
+
+use OCP\Files\File;
+
+/**
+ * This interface is implemented by apps that provide
+ * a file conversion provider
+ *
+ * @since 31.0.0
+ */
+interface IConversionProvider {
+ /**
+ * Get an array of MIME type tuples this conversion provider supports
+ *
+ * @return list<ConversionMimeTuple>
+ *
+ * @since 31.0.0
+ */
+ public function getSupportedMimeTypes(): array;
+
+ /**
+ * Convert a file to a given MIME type
+ *
+ * @param File $file The file to be converted
+ * @param string $targetMimeType The MIME type to convert the file to
+ *
+ * @return resource|string Resource or string content of the file
+ *
+ * @since 31.0.0
+ */
+ public function convertFile(File $file, string $targetMimeType): mixed;
+}