Skip to content

Commit

Permalink
feat(api): basic file conversion api
Browse files Browse the repository at this point in the history
Signed-off-by: Elizabeth Danzberger <[email protected]>
  • Loading branch information
elzody committed Dec 23, 2024
1 parent a60f71b commit c06b2f3
Show file tree
Hide file tree
Showing 10 changed files with 365 additions and 0 deletions.
61 changes: 61 additions & 0 deletions core/Controller/ConversionApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/


namespace OC\Core\Controller;

use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\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 $conversionManager,
private IRootFolder $rootFolder,
private IL10N $l10n,
private ?string $userId,
) {
parent::__construct($appName, $request);
}

#[UserRateLimit(limit: 25, period: 120)]
#[ApiRoute(verb: 'POST', url: '/convert', root: '/conversion')]
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)) {
return new DataResponse([
'message' => $this->l10n->t('File not found'),
], Http::STATUS_NOT_FOUND);
}

try {
$destination = $userFolder->getFullpath($destination);
$convertedFile = $this->conversionManager->convert($file, $targetMimeType, $destination);
} catch (\Exception $e) {
return new DataResponse([
'message' => $e->getMessage(),
], Http::STATUS_INTERNAL_SERVER_ERROR);
}

return new DataResponse([
'path' => $convertedFile,
], Http::STATUS_CREATED);
}
}
5 changes: 5 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@
'OCP\\Contacts\\ContactsMenu\\IProvider' => $baseDir . '/lib/public/Contacts/ContactsMenu/IProvider.php',
'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => $baseDir . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php',
'OCP\\Contacts\\IManager' => $baseDir . '/lib/public/Contacts/IManager.php',
'OCP\\Conversion\\ConversionMimeTuple' => $baseDir . '/lib/public/Conversion/ConversionMimeTuple.php',
'OCP\\Conversion\\IConversionManager' => $baseDir . '/lib/public/Conversion/IConversionManager.php',
'OCP\\Conversion\\IConversionProvider' => $baseDir . '/lib/public/Conversion/IConversionProvider.php',
'OCP\\DB\\Events\\AddMissingColumnsEvent' => $baseDir . '/lib/public/DB/Events/AddMissingColumnsEvent.php',
'OCP\\DB\\Events\\AddMissingIndicesEvent' => $baseDir . '/lib/public/DB/Events/AddMissingIndicesEvent.php',
'OCP\\DB\\Events\\AddMissingPrimaryKeyEvent' => $baseDir . '/lib/public/DB/Events/AddMissingPrimaryKeyEvent.php',
Expand Down Expand Up @@ -1165,6 +1168,7 @@
'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php',
'OC\\Contacts\\ContactsMenu\\Providers\\LocalTimeProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/LocalTimeProvider.php',
'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php',
'OC\\Conversion\\ConversionManager' => $baseDir . '/lib/private/Conversion/ConversionManager.php',
'OC\\Core\\Application' => $baseDir . '/core/Application.php',
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => $baseDir . '/core/BackgroundJobs/CheckForUserCertificates.php',
Expand Down Expand Up @@ -1303,6 +1307,7 @@
'OC\\Core\\Controller\\ClientFlowLoginV2Controller' => $baseDir . '/core/Controller/ClientFlowLoginV2Controller.php',
'OC\\Core\\Controller\\CollaborationResourcesController' => $baseDir . '/core/Controller/CollaborationResourcesController.php',
'OC\\Core\\Controller\\ContactsMenuController' => $baseDir . '/core/Controller/ContactsMenuController.php',
'OC\\Core\\Controller\\ConversionApiController' => $baseDir . '/core/Controller/ConversionApiController.php',
'OC\\Core\\Controller\\CssController' => $baseDir . '/core/Controller/CssController.php',
'OC\\Core\\Controller\\ErrorController' => $baseDir . '/core/Controller/ErrorController.php',
'OC\\Core\\Controller\\GuestAvatarController' => $baseDir . '/core/Controller/GuestAvatarController.php',
Expand Down
5 changes: 5 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Contacts\\ContactsMenu\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IProvider.php',
'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => __DIR__ . '/../../..' . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php',
'OCP\\Contacts\\IManager' => __DIR__ . '/../../..' . '/lib/public/Contacts/IManager.php',
'OCP\\Conversion\\ConversionMimeTuple' => __DIR__ . '/../../..' . '/lib/public/Conversion/ConversionMimeTuple.php',
'OCP\\Conversion\\IConversionManager' => __DIR__ . '/../../..' . '/lib/public/Conversion/IConversionManager.php',
'OCP\\Conversion\\IConversionProvider' => __DIR__ . '/../../..' . '/lib/public/Conversion/IConversionProvider.php',
'OCP\\DB\\Events\\AddMissingColumnsEvent' => __DIR__ . '/../../..' . '/lib/public/DB/Events/AddMissingColumnsEvent.php',
'OCP\\DB\\Events\\AddMissingIndicesEvent' => __DIR__ . '/../../..' . '/lib/public/DB/Events/AddMissingIndicesEvent.php',
'OCP\\DB\\Events\\AddMissingPrimaryKeyEvent' => __DIR__ . '/../../..' . '/lib/public/DB/Events/AddMissingPrimaryKeyEvent.php',
Expand Down Expand Up @@ -1214,6 +1217,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php',
'OC\\Contacts\\ContactsMenu\\Providers\\LocalTimeProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/LocalTimeProvider.php',
'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php',
'OC\\Conversion\\ConversionManager' => __DIR__ . '/../../..' . '/lib/private/Conversion/ConversionManager.php',
'OC\\Core\\Application' => __DIR__ . '/../../..' . '/core/Application.php',
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CheckForUserCertificates.php',
Expand Down Expand Up @@ -1352,6 +1356,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Controller\\ClientFlowLoginV2Controller' => __DIR__ . '/../../..' . '/core/Controller/ClientFlowLoginV2Controller.php',
'OC\\Core\\Controller\\CollaborationResourcesController' => __DIR__ . '/../../..' . '/core/Controller/CollaborationResourcesController.php',
'OC\\Core\\Controller\\ContactsMenuController' => __DIR__ . '/../../..' . '/core/Controller/ContactsMenuController.php',
'OC\\Core\\Controller\\ConversionApiController' => __DIR__ . '/../../..' . '/core/Controller/ConversionApiController.php',
'OC\\Core\\Controller\\CssController' => __DIR__ . '/../../..' . '/core/Controller/CssController.php',
'OC\\Core\\Controller\\ErrorController' => __DIR__ . '/../../..' . '/core/Controller/ErrorController.php',
'OC\\Core\\Controller\\GuestAvatarController' => __DIR__ . '/../../..' . '/core/Controller/GuestAvatarController.php',
Expand Down
25 changes: 25 additions & 0 deletions lib/private/AppFramework/Bootstrap/RegistrationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ class RegistrationContext {

/** @var ServiceRegistration<\OCP\TaskProcessing\ITaskType>[] */
private array $taskProcessingTaskTypes = [];

/** @var ServiceRegistration<\OCP\Conversion\IConversionProvider>[] */
private array $conversionProviders = [];

/** @var ServiceRegistration<IMailProvider>[] */
private $mailProviders = [];
Expand Down Expand Up @@ -420,6 +423,13 @@ public function registerTaskProcessingTaskType(string $taskProcessingTaskTypeCla
);
}

public function registerConversionProvider(string $conversionProviderClass): void {
$this->context->registerConversionProvider(
$this->appId,
$conversionProviderClass
);
}

public function registerMailProvider(string $class): void {
$this->context->registerMailProvider(
$this->appId,
Expand Down Expand Up @@ -625,6 +635,14 @@ public function registerTaskProcessingProvider(string $appId, string $taskProces
public function registerTaskProcessingTaskType(string $appId, string $taskProcessingTaskTypeClass) {
$this->taskProcessingTaskTypes[] = new ServiceRegistration($appId, $taskProcessingTaskTypeClass);
}

/**
* @psalm-param class-string<\OCP\Conversion\IConversionProvider> $conversionProviderClass
*/
public function registerConversionProvider(string $appId, string $conversionProviderClass): void {
$this->conversionProviders[] = new ServiceRegistration($appId, $conversionProviderClass);
}

/**
* @psalm-param class-string<IMailProvider> $migratorClass
*/
Expand Down Expand Up @@ -984,6 +1002,13 @@ public function getTaskProcessingTaskTypes(): array {
return $this->taskProcessingTaskTypes;
}

/**
* @return ServiceRegistration<\OCP\Conversion\IConversionProvider>[]
*/
public function getConversionProviders(): array {
return $this->conversionProviders;
}

/**
* @return ServiceRegistration<IMailProvider>[]
*/
Expand Down
115 changes: 115 additions & 0 deletions lib/private/Conversion/ConversionManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Conversion;

use OC\AppFramework\Bootstrap\Coordinator;
use OCP\Conversion\ConversionMimeTuple;
use OCP\Conversion\IConversionManager;
use OCP\Conversion\IConversionProvider;
use OCP\Files\File;
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 ?IConversionProvider[] */
private ?array $providers = null;

public function __construct(
private Coordinator $coordinator,
private ContainerInterface $serverContainer,
private IRootFolder $rootFolder,
private ITempManager $tempManager,
private LoggerInterface $logger,
) {
}

public function hasProviders(): bool {
$context = $this->coordinator->getRegistrationContext();
return !empty($context->getConversionProviders());
}

public function getMimeTypes(): array {
$mimeTypes = [];

foreach ($this->getProviders() as $provider) {
$mimeTypes[] = $provider->getSupportedMimeTypes();
}

return $mimeTypes;
}

public function convert(File $file, string $targetMimeType, ?string $destination = null): string {
if (!$this->hasProviders()) {
throw new PreConditionNotMetException('No conversion providers available');
}

$fileMimeType = $file->getMimetype();
foreach ($this->getProviders() as $provider) {
$availableProviderConversions = array_filter(
$provider->getSupportedMimeTypes(),
function (ConversionMimeTuple $mimeTuple) use ($fileMimeType, $targetMimeType) {
['from' => $from, 'to' => $to] = $mimeTuple->jsonSerialize();

return $from === $fileMimeType && $to === $targetMimeType;
}
);

if (!empty($availableProviderConversions)) {
$convertedFile = $provider->convertFile($file, $targetMimeType);

if ($destination !== null) {
$convertedFile = $this->writeToDestination($destination, $convertedFile);
return $convertedFile->getInternalPath();
}

$tmp = $this->tempManager->getTemporaryFile();
file_put_contents($tmp, $convertedFile);

return $tmp;
}
}

throw new RuntimeException('Could not convert file');
}

public function getProviders(): array {
if ($this->providers !== null) {
return $this->providers;
}

$context = $this->coordinator->getRegistrationContext();
$this->providers = [];

foreach ($context->getConversionProviders() as $providerRegistration) {
$class = $providerRegistration->getService();

try {
$this->providers[$class] = $this->serverContainer->get($class);
} catch (NotFoundExceptionInterface|ContainerExceptionInterface|Throwable $e) {
$this->logger->error('Failed to load conversion provider ' . $class, [
'exception' => $e,
]);
}
}

return $this->providers;
}

private function writeToDestination(string $destination, mixed $content): File {
return $this->rootFolder->newFile($destination, $content);
}
}
4 changes: 4 additions & 0 deletions lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use OC\Comments\ManagerFactory as CommentsManagerFactory;
use OC\Contacts\ContactsMenu\ActionFactory;
use OC\Contacts\ContactsMenu\ContactsStore;
use OC\Conversion\ConversionManager;
use OC\DB\Connection;
use OC\DB\ConnectionAdapter;
use OC\Diagnostics\EventLogger;
Expand Down Expand Up @@ -143,6 +144,7 @@
use OCP\Comments\ICommentsManager;
use OCP\Contacts\ContactsMenu\IActionFactory;
use OCP\Contacts\ContactsMenu\IContactsStore;
use OCP\Conversion\IConversionManager;
use OCP\Defaults;
use OCP\Diagnostics\IEventLogger;
use OCP\Diagnostics\IQueryLogger;
Expand Down Expand Up @@ -1258,6 +1260,8 @@ public function __construct($webRoot, \OC\Config $config) {

$this->registerAlias(ITranslationManager::class, TranslationManager::class);

$this->registerAlias(IConversionManager::class, ConversionManager::class);

$this->registerAlias(ISpeechToTextManager::class, SpeechToTextManager::class);

$this->registerAlias(IEventSourceFactory::class, EventSourceFactory::class);
Expand Down
13 changes: 13 additions & 0 deletions lib/public/AppFramework/Bootstrap/IRegistrationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,19 @@ public function registerTaskProcessingProvider(string $taskProcessingProviderCla
*/
public function registerTaskProcessingTaskType(string $taskProcessingTaskTypeClass): void;

/**
* Register an implementation of \OCP\Conversion\IConversionProvider
* that will handle the conversion of files from one MIME type to another
*
* @param string $conversionProviderClass
* @psalm-param class-string<\OCP\Conversion\IConversionProvider> $conversionProviderClass
*
* @return void
*
* @since 31.0.0
*/
public function registerConversionProvider(string $conversionProviderClass): void;

/**
* Register a mail provider
*
Expand Down
39 changes: 39 additions & 0 deletions lib/public/Conversion/ConversionMimeTuple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\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 string $to The desired MIME type for the file
*
* @since 31.0.0
*/
public function __construct(
private string $from,
private string $to,
) {
}

public function jsonSerialize(): array {
return [
'from' => $this->from,
'to' => $this->to,
];
}
}
Loading

0 comments on commit c06b2f3

Please sign in to comment.