feat(api): File conversion API

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>
pull/49922/head
Elizabeth Danzberger 1 year ago
parent 6da58974a1
commit fdfeb7f265
No known key found for this signature in database
GPG Key ID: D64CE07FD0188C79
  1. 1
      apps/files/composer/composer/autoload_classmap.php
  2. 1
      apps/files/composer/composer/autoload_static.php
  3. 9
      apps/files/lib/Capabilities.php
  4. 95
      apps/files/lib/Controller/ConversionApiController.php
  5. 149
      apps/files/openapi.json
  6. 91
      apps/files/tests/Controller/ConversionApiControllerTest.php
  7. 1
      apps/testing/composer/composer/autoload_classmap.php
  8. 1
      apps/testing/composer/composer/autoload_static.php
  9. 3
      apps/testing/lib/AppInfo/Application.php
  10. 40
      apps/testing/lib/Conversion/ConversionProvider.php
  11. 8
      config/config.sample.php
  12. 4
      lib/composer/composer/autoload_classmap.php
  13. 4
      lib/composer/composer/autoload_static.php
  14. 25
      lib/private/AppFramework/Bootstrap/RegistrationContext.php
  15. 2
      lib/private/CapabilitiesManager.php
  16. 152
      lib/private/Files/Conversion/ConversionManager.php
  17. 4
      lib/private/Server.php
  18. 13
      lib/public/AppFramework/Bootstrap/IRegistrationContext.php
  19. 44
      lib/public/Files/Conversion/ConversionMimeTuple.php
  20. 46
      lib/public/Files/Conversion/IConversionManager.php
  21. 41
      lib/public/Files/Conversion/IConversionProvider.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',

@ -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',

@ -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()),
],
];
}

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

@ -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": []

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

@ -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',

@ -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',

@ -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);

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

@ -1428,6 +1428,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
*

@ -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',

@ -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',

@ -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
*/
@ -985,6 +1003,13 @@ class RegistrationContext {
return $this->taskProcessingTaskTypes;
}
/**
* @return ServiceRegistration<\OCP\Files\Conversion\IConversionProvider>[]
*/
public function getFileConversionProviders(): array {
return $this->fileConversionProviders;
}
/**
* @return ServiceRegistration<IMailProvider>[]
*/

@ -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

@ -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;
}
}

@ -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);

@ -414,6 +414,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
*

@ -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,
];
}
}

@ -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;
}

@ -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;
}
Loading…
Cancel
Save