Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>pull/49922/head
parent
6da58974a1
commit
fdfeb7f265
@ -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; |
||||
} |
||||
Loading…
Reference in new issue