Browse Source
feat(api): File conversion API
feat(api): File conversion API
Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>pull/49922/head
No known key found for this signature in database
GPG Key ID: D64CE07FD0188C79
21 changed files with 732 additions and 2 deletions
-
1apps/files/composer/composer/autoload_classmap.php
-
1apps/files/composer/composer/autoload_static.php
-
9apps/files/lib/Capabilities.php
-
95apps/files/lib/Controller/ConversionApiController.php
-
149apps/files/openapi.json
-
91apps/files/tests/Controller/ConversionApiControllerTest.php
-
1apps/testing/composer/composer/autoload_classmap.php
-
1apps/testing/composer/composer/autoload_static.php
-
3apps/testing/lib/AppInfo/Application.php
-
40apps/testing/lib/Conversion/ConversionProvider.php
-
8config/config.sample.php
-
4lib/composer/composer/autoload_classmap.php
-
4lib/composer/composer/autoload_static.php
-
25lib/private/AppFramework/Bootstrap/RegistrationContext.php
-
2lib/private/CapabilitiesManager.php
-
152lib/private/Files/Conversion/ConversionManager.php
-
4lib/private/Server.php
-
13lib/public/AppFramework/Bootstrap/IRegistrationContext.php
-
44lib/public/Files/Conversion/ConversionMimeTuple.php
-
46lib/public/Files/Conversion/IConversionManager.php
-
41lib/public/Files/Conversion/IConversionProvider.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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue