diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2025-01-16 19:31:14 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-16 19:31:14 +0100 |
commit | 35db02c4f92124acbe96bb76d37f0595f87bc924 (patch) | |
tree | 62c8a1ef407e80881e0405ddfae98514b150dd78 | |
parent | d9a7124592beaab371d7c39e792f3b3e8d0dd6fa (diff) | |
parent | 19ce3628965f73f21beac3fc9ee3757e091313c4 (diff) | |
download | nextcloud-server-35db02c4f92124acbe96bb76d37f0595f87bc924.tar.gz nextcloud-server-35db02c4f92124acbe96bb76d37f0595f87bc924.zip |
Merge pull request #50208 from nextcloud/feat/conversion-adjusting
fix(files): conversion api simplification and conflict check
18 files changed, 358 insertions, 126 deletions
diff --git a/.github/workflows/integration-sqlite.yml b/.github/workflows/integration-sqlite.yml index b16e64b21da..d19e45600ae 100644 --- a/.github/workflows/integration-sqlite.yml +++ b/.github/workflows/integration-sqlite.yml @@ -61,6 +61,7 @@ jobs: - 'federation_features' - '--tags ~@large files_features' - 'filesdrop_features' + - 'file_conversions' - 'openldap_features' - 'openldap_numerical_features' - 'ldap_features' diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index 88efb4fcaf0..6b50e5807a5 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -10,7 +10,7 @@ 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\ConversionMimeProvider; use OCP\Files\Conversion\IConversionManager; class Capabilities implements ICapability { @@ -24,7 +24,7 @@ class Capabilities implements ICapability { /** * 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}, file_conversions: list<array{from: string, to: list<array{mime: string, name: string}>}>}} + * @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: string, extension: string, displayName: string}>}} */ public function getCapabilities(): array { return [ @@ -42,9 +42,9 @@ class Capabilities implements ICapability { 'max_parallel_count' => ChunkedUploadConfig::getMaxParallelCount(), ], - 'file_conversions' => array_map(function (ConversionMimeTuple $mimeTuple) { - return $mimeTuple->jsonSerialize(); - }, $this->fileConversionManager->getMimeTypes()), + 'file_conversions' => array_map(function (ConversionMimeProvider $mimeProvider) { + return $mimeProvider->jsonSerialize(); + }, $this->fileConversionManager->getProviders()), ], ]; } diff --git a/apps/files/openapi.json b/apps/files/openapi.json index 015d4f42648..ddf3535c55f 100644 --- a/apps/files/openapi.json +++ b/apps/files/openapi.json @@ -101,29 +101,22 @@ "type": "object", "required": [ "from", - "to" + "to", + "extension", + "displayName" ], "properties": { "from": { "type": "string" }, "to": { - "type": "array", - "items": { - "type": "object", - "required": [ - "mime", - "name" - ], - "properties": { - "mime": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } + "type": "string" + }, + "extension": { + "type": "string" + }, + "displayName": { + "type": "string" } } } diff --git a/apps/testing/lib/Conversion/ConversionProvider.php b/apps/testing/lib/Conversion/ConversionProvider.php index 15e468b0756..b8d93428694 100644 --- a/apps/testing/lib/Conversion/ConversionProvider.php +++ b/apps/testing/lib/Conversion/ConversionProvider.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace OCA\Testing\Conversion; -use OCP\Files\Conversion\ConversionMimeTuple; +use OCP\Files\Conversion\ConversionMimeProvider; use OCP\Files\Conversion\IConversionProvider; use OCP\Files\File; use OCP\IL10N; @@ -22,19 +22,26 @@ class ConversionProvider implements IConversionProvider { public function getSupportedMimeTypes(): array { return [ - new ConversionMimeTuple('image/jpeg', [ - ['mime' => 'image/png', 'name' => $this->l10n->t('Image (.png)')], - ]) + new ConversionMimeProvider('image/jpeg', 'image/png', 'png', $this->l10n->t('Image (.png)')), + new ConversionMimeProvider('image/jpeg', 'image/gif', 'gif', $this->l10n->t('Image (.gif)')), ]; } public function convertFile(File $file, string $targetMimeType): mixed { $image = imagecreatefromstring($file->getContent()); - imagepalettetotruecolor($image); + // Start output buffering ob_start(); - imagepng($image); + + // Convert the image to the target format + if ($targetMimeType === 'image/gif') { + imagegif($image); + } else { + imagepng($image); + } + + // End and return the output buffer return ob_get_clean(); } } diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index 21e0cdb3309..47f8f3c827c 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -16,7 +16,7 @@ default: - "%paths.base%/../features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -39,7 +39,7 @@ default: - "%paths.base%/../comments_features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -62,7 +62,7 @@ default: - "%paths.base%/../dav_features" contexts: - DavFeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -85,7 +85,7 @@ default: - "%paths.base%/../federation_features" contexts: - FederationContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -95,7 +95,7 @@ default: - "%paths.base%/../files_features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -113,12 +113,22 @@ default: - CommandLineContext: baseUrl: http://localhost:8080 ocPath: ../../ + files_conversion: + paths: + - "%paths.base%/../file_conversions" + contexts: + - ConversionsContext: + baseUrl: http://localhost:8080 + admin: + - admin + - admin + regular_user_password: 123456 capabilities: paths: - "%paths.base%/../capabilities_features" contexts: - CapabilitiesContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -128,7 +138,7 @@ default: - "%paths.base%/../collaboration_features" contexts: - CollaborationContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -138,7 +148,7 @@ default: - "%paths.base%/../sharees_features" contexts: - ShareesContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -148,7 +158,7 @@ default: - "%paths.base%/../sharing_features" contexts: - SharingContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -159,7 +169,7 @@ default: - "%paths.base%/../videoverification_features" contexts: - SharingContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -170,7 +180,7 @@ default: - "%paths.base%/../setup_features" contexts: - SetupContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin @@ -220,10 +230,10 @@ default: - "%paths.base%/../remoteapi_features" contexts: - FeatureContext: - baseUrl: http://localhost:8080/ocs/ + baseUrl: http://localhost:8080/ocs/ admin: - admin - admin regular_user_password: 123456 - RemoteContext: - remote: http://localhost:8080 + remote: http://localhost:8080 diff --git a/build/integration/data/clouds.jpg b/build/integration/data/clouds.jpg Binary files differnew file mode 100644 index 00000000000..2433b140766 --- /dev/null +++ b/build/integration/data/clouds.jpg diff --git a/build/integration/data/clouds.jpg.license b/build/integration/data/clouds.jpg.license new file mode 100644 index 00000000000..d7c54c39d02 --- /dev/null +++ b/build/integration/data/clouds.jpg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2019 CHUTTERSNAP <https://unsplash.com/@chuttersnap> <https://unsplash.com/photos/blue-clouds-under-white-sky-9AqIdzEc9pY>" +SPDX-License-Identifier: LicenseRef-Unsplash diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index b51c0a6a34c..60926e65f5c 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -8,6 +8,7 @@ use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; @@ -170,6 +171,8 @@ trait BasicStructure { $this->response = $client->request($verb, $fullUrl, $options); } catch (ClientException $ex) { $this->response = $ex->getResponse(); + } catch (ServerException $ex) { + $this->response = $ex->getResponse(); } } diff --git a/build/integration/features/bootstrap/ConversionsContext.php b/build/integration/features/bootstrap/ConversionsContext.php new file mode 100644 index 00000000000..099a2263630 --- /dev/null +++ b/build/integration/features/bootstrap/ConversionsContext.php @@ -0,0 +1,59 @@ +<?php +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +require __DIR__ . '/../../vendor/autoload.php'; + +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; +use Behat\Gherkin\Node\TableNode; + +class ConversionsContext implements Context, SnippetAcceptingContext { + use AppConfiguration; + use BasicStructure; + use WebDav; + + /** @BeforeScenario */ + public function setUpScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(true); + } + + /** @AfterScenario */ + public function tearDownScenario() { + $this->asAn('admin'); + $this->setStatusTestingApp(false); + } + + protected function resetAppConfigs() { + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileId(string $user, string $path, string $mime) { + $this->userConvertsTheSavedFileIdTo($user, $path, $mime, null); + } + + /** + * @When /^user "([^"]*)" converts file "([^"]*)" to "([^"]*)" and saves it to "([^"]*)"$/ + */ + public function userConvertsTheSavedFileIdTo(string $user, string $path, string $mime, ?string $destination) { + try { + $fileId = $this->getFileIdForPath($user, $path); + } catch (Exception $e) { + // return a fake value to keep going and be able to test the error + $fileId = 0; + } + + $data = [['fileId', $fileId], ['targetMimeType', $mime]]; + if ($destination !== null) { + $data[] = ['destination', $destination]; + } + + $this->asAn($user); + $this->sendingToWith('post', '/apps/files/api/v1/convert', new TableNode($data)); + } +} diff --git a/build/integration/file_conversions/file_conversions.feature b/build/integration/file_conversions/file_conversions.feature new file mode 100644 index 00000000000..f56dca0ebb4 --- /dev/null +++ b/build/integration/file_conversions/file_conversions.feature @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-only + +Feature: conversions + Background: + Given using api version "2" + Given using new dav path + Given user "user0" exists + + Scenario: Converting a file works + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + Then as "user0" the file "/image.jpg" exists + When user "user0" converts file "/image.jpg" to "image/png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/image.png" exists + + Scenario: Converting a file to a given path works + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + And User "user0" created a folder "/folder" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the folder "/folder" exists + When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/folder/image.png" exists + Then as "user0" the file "/image.png" does not exist + + Scenario: Converting a file path with overwrite + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + And user "user0" uploads file "data/green-square-256.png" to "/image.png" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/image.png" exists + When user "user0" converts file "/image.jpg" to "image/png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/image.png" exists + Then as "user0" the file "/image (2).png" exists + + Scenario: Converting a file path with overwrite to a given path + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + And User "user0" created a folder "/folder" + And user "user0" uploads file "data/green-square-256.png" to "/folder/image.png" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the folder "/folder" exists + Then as "user0" the file "/folder/image.png" exists + When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png" + Then the HTTP status code should be "201" + Then the OCS status code should be "201" + Then as "user0" the file "/folder/image.png" exists + Then as "user0" the file "/folder/image (2).png" exists + Then as "user0" the file "/image.png" does not exist + Then as "user0" the file "/image.jpg" exists + + Scenario: Converting a file which does not exist fails + When user "user0" converts file "/image.jpg" to "image/png" + Then the HTTP status code should be "404" + Then the OCS status code should be "404" + Then as "user0" the file "/image.jpg" does not exist + Then as "user0" the file "/image.png" does not exist + + Scenario: Converting a file to an invalid destination path fails + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image.png" + Then the HTTP status code should be "404" + Then the OCS status code should be "404" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/folder/image.png" does not exist + + Scenario: Converting a file to an invalid format fails + Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg" + When user "user0" converts file "/image.jpg" to "image/invalid" + Then the HTTP status code should be "500" + Then the OCS status code should be "999" + Then as "user0" the file "/image.jpg" exists + Then as "user0" the file "/image.png" does not exist + + Scenario: Forbid conversion to a destination without create permission + Given user "user1" exists + # Share the folder with user1 + Given User "user0" created a folder "/folder" + Then As an "user0" + When creating a share with + | path | folder | + | shareWith | user1 | + | shareType | 0 | + | permissions | 1 | + Then the OCS status code should be "200" + And the HTTP status code should be "200" + # Create the folder, upload the image + Then As an "user1" + Given user "user1" accepts last share + Given as "user1" the folder "/folder" exists + Given user "user1" uploads file "data/clouds.jpg" to "/image.jpg" + Then as "user1" the file "/image.jpg" exists + # Try to convert the image to a folder where user1 has no create permission + When user "user1" converts file "/image.jpg" to "image/png" and saves it to "/folder/folder.png" + Then the OCS status code should be "403" + And the HTTP status code should be "403" + Then as "user1" the file "/folder/folder.png" does not exist diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index dab71032ccc..d20e3f9f501 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -377,7 +377,7 @@ 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\\ConversionMimeProvider' => $baseDir . '/lib/public/Files/Conversion/ConversionMimeProvider.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', diff --git a/lib/composer/composer/autoload_psr4.php b/lib/composer/composer/autoload_psr4.php index b07b2c0074b..09892528d3a 100644 --- a/lib/composer/composer/autoload_psr4.php +++ b/lib/composer/composer/autoload_psr4.php @@ -10,5 +10,6 @@ return array( 'OC\\' => array($baseDir . '/lib/private'), 'OCP\\' => array($baseDir . '/lib/public'), 'NCU\\' => array($baseDir . '/lib/unstable'), + 'Bamarni\\Composer\\Bin\\' => array($vendorDir . '/bamarni/composer-bin-plugin/src'), '' => array($baseDir . '/lib/private/legacy'), ); diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 00d3b2c2b5a..ead08210def 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -21,6 +21,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 array ( 'NCU\\' => 4, ), + 'B' => + array ( + 'Bamarni\\Composer\\Bin\\' => 21, + ), ); public static $prefixDirsPsr4 = array ( @@ -40,6 +44,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 array ( 0 => __DIR__ . '/../../..' . '/lib/unstable', ), + 'Bamarni\\Composer\\Bin\\' => + array ( + 0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src', + ), ); public static $fallbackDirsPsr4 = array ( @@ -418,7 +426,7 @@ 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\\ConversionMimeProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Conversion/ConversionMimeProvider.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', diff --git a/lib/private/Files/Conversion/ConversionManager.php b/lib/private/Files/Conversion/ConversionManager.php index 37fc9a6c754..db49f7afed9 100644 --- a/lib/private/Files/Conversion/ConversionManager.php +++ b/lib/private/Files/Conversion/ConversionManager.php @@ -11,7 +11,7 @@ namespace OC\Files\Conversion; use OC\AppFramework\Bootstrap\Coordinator; use OC\SystemConfig; -use OCP\Files\Conversion\ConversionMimeTuple; +use OCP\Files\Conversion\ConversionMimeProvider; use OCP\Files\Conversion\IConversionManager; use OCP\Files\Conversion\IConversionProvider; use OCP\Files\File; @@ -32,10 +32,10 @@ class ConversionManager implements IConversionManager { 'richdocuments', ]; - /** @var IConversionProvider[] */ + /** @var list<IConversionProvider> */ private array $preferredProviders = []; - /** @var IConversionProvider[] */ + /** @var list<IConversionProvider> */ private array $providers = []; public function __construct( @@ -53,16 +53,28 @@ class ConversionManager implements IConversionManager { return !empty($context->getFileConversionProviders()); } - public function getMimeTypes(): array { - $mimeTypes = []; - - foreach ($this->getProviders() as $provider) { - $mimeTypes[] = $provider->getSupportedMimetypes(); + public function getProviders(): array { + $providers = []; + foreach ($this->getRegisteredProviders() as $provider) { + $providers = array_merge($providers, $provider->getSupportedMimeTypes()); } + return $providers; + } + + /** + * @param string $mime + * @return list<ConversionMimeProvider> + */ + private function getProvidersForMime(string $mime): array { + $mimeTypes = $this->getProviders(); + $filtered = array_filter( + $mimeTypes, + function (ConversionMimeProvider $mimeProvider) use ($mime) { + return $mimeProvider->getFrom() === $mime; + } + ); - /** @var list<ConversionMimeTuple> */ - $mimeTypes = array_merge(...$mimeTypes); - return $mimeTypes; + return array_values($filtered); } public function convert(File $file, string $targetMimeType, ?string $destination = null): string { @@ -80,24 +92,36 @@ class ConversionManager implements IConversionManager { $fileMimeType = $file->getMimetype(); $validProvider = $this->getValidProvider($fileMimeType, $targetMimeType); + $targetExtension = ''; + foreach ($this->getProvidersForMime($fileMimeType) as $mimeProvider) { + if ($mimeProvider->getTo() === $targetMimeType) { + $targetExtension = $mimeProvider->getExtension(); + break; + } + } + if ($validProvider !== null) { $convertedFile = $validProvider->convertFile($file, $targetMimeType); - if ($destination !== null) { - $convertedFile = $this->writeToDestination($destination, $convertedFile); - return $convertedFile->getPath(); + // If destination not provided, we use the same path + // as the original file, but with the new extension + if ($destination === null) { + $basename = pathinfo($file->getPath(), PATHINFO_FILENAME); + $parent = $file->getParent(); + $destination = $parent->getFullPath($basename . '.' . $targetExtension); } - $tmp = $this->tempManager->getTemporaryFile(); - file_put_contents($tmp, $convertedFile); - - return $tmp; + $convertedFile = $this->writeToDestination($destination, $convertedFile); + return $convertedFile->getPath(); } throw new RuntimeException('Could not convert file'); } - public function getProviders(): array { + /** + * @return list<IConversionProvider> + */ + private function getRegisteredProviders(): array { if (count($this->providers) > 0) { return $this->providers; } @@ -121,32 +145,33 @@ class ConversionManager implements IConversionManager { } } - return array_merge([], $this->preferredProviders, $this->providers); + return array_values(array_merge([], $this->preferredProviders, $this->providers)); } private function writeToDestination(string $destination, mixed $content): File { + if ($this->rootFolder->nodeExists($destination)) { + $file = $this->rootFolder->get($destination); + $parent = $file->getParent(); + if (!$parent->isCreatable()) { + throw new GenericFileException('Destination is not creatable'); + } + + $newName = $parent->getNonExistingName(basename($destination)); + $destination = $parent->getFullPath($newName); + } + 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; + foreach ($this->getRegisteredProviders() as $provider) { + foreach ($provider->getSupportedMimeTypes() as $mimeProvider) { + if ($mimeProvider->getFrom() === $fileMimeType && $mimeProvider->getTo() === $targetMimeType) { + return $provider; } - ); - - if (!empty($suitableMimeTypes)) { - $validProvider = $provider; - break; } } - - return $validProvider; + + return null; } } diff --git a/lib/public/Files/Conversion/ConversionMimeProvider.php b/lib/public/Files/Conversion/ConversionMimeProvider.php new file mode 100644 index 00000000000..0daf4a10648 --- /dev/null +++ b/lib/public/Files/Conversion/ConversionMimeProvider.php @@ -0,0 +1,66 @@ +<?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 ConversionMimeProvider implements JsonSerializable { + /** + * @param string $from The source MIME type of a file + * @param string $to The target MIME type for the file + * @param string $extension The file extension for the target MIME type (e.g. 'png') + * @param string $displayName The human-readable name of the target MIME type (e.g. 'Image (.png)') + * + * @since 31.0.0 + */ + public function __construct( + private string $from, + private string $to, + private string $extension, + private string $displayName, + ) { + } + + public function getFrom(): string { + return $this->from; + } + + public function getTo(): string { + return $this->to; + } + + public function getExtension(): string { + return $this->extension; + } + + public function getDisplayName(): string { + return $this->displayName; + } + + /** + * @return array{from: string, to: string, extension: string, displayName: string} + * + * @since 31.0.0 + */ + public function jsonSerialize(): array { + return [ + 'from' => $this->from, + 'to' => $this->to, + 'extension' => $this->extension, + 'displayName' => $this->displayName, + ]; + } +} diff --git a/lib/public/Files/Conversion/ConversionMimeTuple.php b/lib/public/Files/Conversion/ConversionMimeTuple.php deleted file mode 100644 index 0180f3311f3..00000000000 --- a/lib/public/Files/Conversion/ConversionMimeTuple.php +++ /dev/null @@ -1,44 +0,0 @@ -<?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 index 59ff580fdf1..ed418129d3b 100644 --- a/lib/public/Files/Conversion/IConversionManager.php +++ b/lib/public/Files/Conversion/IConversionManager.php @@ -25,11 +25,11 @@ interface IConversionManager { /** * Gets all supported MIME type conversions * - * @return list<ConversionMimeTuple> + * @return list<ConversionMimeProvider> * * @since 31.0.0 */ - public function getMimeTypes(): array; + public function getProviders(): array; /** * Convert a file to a given MIME type diff --git a/lib/public/Files/Conversion/IConversionProvider.php b/lib/public/Files/Conversion/IConversionProvider.php index b0a09fc93a4..3b5c5945c99 100644 --- a/lib/public/Files/Conversion/IConversionProvider.php +++ b/lib/public/Files/Conversion/IConversionProvider.php @@ -21,7 +21,7 @@ interface IConversionProvider { /** * Get an array of MIME type tuples this conversion provider supports * - * @return list<ConversionMimeTuple> + * @return list<ConversionMimeProvider> * * @since 31.0.0 */ |