diff options
author | Vincent Petry <vincent@nextcloud.com> | 2022-04-14 13:39:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-14 13:39:31 +0200 |
commit | 3ca797129ced4546d4b3d0c5e84a1871decca55c (patch) | |
tree | 11f65b483cc599ccc775252c2c0362afda80459c /lib | |
parent | e827e0a14061a08eb6009e3fa222c2e66a4ffaf6 (diff) | |
parent | 1c7ecfc54499d866b9913c135c0c7439634d8bb1 (diff) | |
download | nextcloud-server-3ca797129ced4546d4b3d0c5e84a1871decca55c.tar.gz nextcloud-server-3ca797129ced4546d4b3d0c5e84a1871decca55c.zip |
Merge pull request #31839 from nextcloud/feat/metadata-server
Add a metadata service to store file metadata
Diffstat (limited to 'lib')
20 files changed, 570 insertions, 10 deletions
diff --git a/lib/composer/autoload.php b/lib/composer/autoload.php index 6de0160c0b5..a3d144b1777 100644 --- a/lib/composer/autoload.php +++ b/lib/composer/autoload.php @@ -2,6 +2,11 @@ // autoload.php @generated by Composer +if (PHP_VERSION_ID < 50600) { + echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; + exit(1); +} + require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit53792487c5a8370acc0b06b1a864ff4c::getLoader(); diff --git a/lib/composer/composer/InstalledVersions.php b/lib/composer/composer/InstalledVersions.php index d50e0c9fcc4..fc50a9f8622 100644 --- a/lib/composer/composer/InstalledVersions.php +++ b/lib/composer/composer/InstalledVersions.php @@ -264,7 +264,7 @@ class InstalledVersions if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 - if (substr(__DIR__, -8, 1) !== 'C') { + if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); @@ -337,7 +337,7 @@ class InstalledVersions if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 - if (substr(__DIR__, -8, 1) !== 'C') { + if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) { self::$installed = require __DIR__ . '/installed.php'; } else { self::$installed = array(); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index acc0f6bf2ad..685fae9bef1 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1021,6 +1021,7 @@ return array( 'OC\\Core\\Migrations\\Version23000Date20211203110726' => $baseDir . '/core/Migrations/Version23000Date20211203110726.php', 'OC\\Core\\Migrations\\Version23000Date20211213203940' => $baseDir . '/core/Migrations/Version23000Date20211213203940.php', 'OC\\Core\\Migrations\\Version240000Date20220202150027' => $baseDir . '/core/Migrations/Version240000Date20220202150027.php', + 'OC\\Core\\Migrations\\Version240000Date20220404230027' => $baseDir . '/core/Migrations/Version240000Date20220404230027.php', 'OC\\Core\\Migrations\\Version24000Date20211210141942' => $baseDir . '/core/Migrations/Version24000Date20211210141942.php', 'OC\\Core\\Migrations\\Version24000Date20211213081506' => $baseDir . '/core/Migrations/Version24000Date20211213081506.php', 'OC\\Core\\Migrations\\Version24000Date20211213081604' => $baseDir . '/core/Migrations/Version24000Date20211213081604.php', @@ -1302,6 +1303,14 @@ return array( 'OC\\Memcache\\ProfilerWrapperCache' => $baseDir . '/lib/private/Memcache/ProfilerWrapperCache.php', 'OC\\Memcache\\Redis' => $baseDir . '/lib/private/Memcache/Redis.php', 'OC\\MemoryInfo' => $baseDir . '/lib/private/MemoryInfo.php', + 'OC\\Metadata\\Capabilities' => $baseDir . '/lib/private/Metadata/Capabilities.php', + 'OC\\Metadata\\FileEventListener' => $baseDir . '/lib/private/Metadata/FileEventListener.php', + 'OC\\Metadata\\FileMetadata' => $baseDir . '/lib/private/Metadata/FileMetadata.php', + 'OC\\Metadata\\FileMetadataMapper' => $baseDir . '/lib/private/Metadata/FileMetadataMapper.php', + 'OC\\Metadata\\IMetadataManager' => $baseDir . '/lib/private/Metadata/IMetadataManager.php', + 'OC\\Metadata\\IMetadataProvider' => $baseDir . '/lib/private/Metadata/IMetadataProvider.php', + 'OC\\Metadata\\MetadataManager' => $baseDir . '/lib/private/Metadata/MetadataManager.php', + 'OC\\Metadata\\Provider\\ExifProvider' => $baseDir . '/lib/private/Metadata/Provider/ExifProvider.php', 'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => $baseDir . '/lib/private/Migration/ConsoleOutput.php', 'OC\\Migration\\SimpleOutput' => $baseDir . '/lib/private/Migration/SimpleOutput.php', diff --git a/lib/composer/composer/autoload_real.php b/lib/composer/composer/autoload_real.php index 4b1ab7678ec..eecff48bcf9 100644 --- a/lib/composer/composer/autoload_real.php +++ b/lib/composer/composer/autoload_real.php @@ -27,7 +27,7 @@ class ComposerAutoloaderInit53792487c5a8370acc0b06b1a864ff4c spl_autoload_unregister(array('ComposerAutoloaderInit53792487c5a8370acc0b06b1a864ff4c', 'loadClassLoader')); require __DIR__ . '/autoload_static.php'; - \Composer\Autoload\ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c::getInitializer($loader)(); + call_user_func(\Composer\Autoload\ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c::getInitializer($loader)); $loader->register(true); diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 09e8d3a627e..7e778d73b83 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1050,6 +1050,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version23000Date20211203110726' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20211203110726.php', 'OC\\Core\\Migrations\\Version23000Date20211213203940' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20211213203940.php', 'OC\\Core\\Migrations\\Version240000Date20220202150027' => __DIR__ . '/../../..' . '/core/Migrations/Version240000Date20220202150027.php', + 'OC\\Core\\Migrations\\Version240000Date20220404230027' => __DIR__ . '/../../..' . '/core/Migrations/Version240000Date20220404230027.php', 'OC\\Core\\Migrations\\Version24000Date20211210141942' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20211210141942.php', 'OC\\Core\\Migrations\\Version24000Date20211213081506' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20211213081506.php', 'OC\\Core\\Migrations\\Version24000Date20211213081604' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20211213081604.php', @@ -1331,6 +1332,14 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Memcache\\ProfilerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/ProfilerWrapperCache.php', 'OC\\Memcache\\Redis' => __DIR__ . '/../../..' . '/lib/private/Memcache/Redis.php', 'OC\\MemoryInfo' => __DIR__ . '/../../..' . '/lib/private/MemoryInfo.php', + 'OC\\Metadata\\Capabilities' => __DIR__ . '/../../..' . '/lib/private/Metadata/Capabilities.php', + 'OC\\Metadata\\FileEventListener' => __DIR__ . '/../../..' . '/lib/private/Metadata/FileEventListener.php', + 'OC\\Metadata\\FileMetadata' => __DIR__ . '/../../..' . '/lib/private/Metadata/FileMetadata.php', + 'OC\\Metadata\\FileMetadataMapper' => __DIR__ . '/../../..' . '/lib/private/Metadata/FileMetadataMapper.php', + 'OC\\Metadata\\IMetadataManager' => __DIR__ . '/../../..' . '/lib/private/Metadata/IMetadataManager.php', + 'OC\\Metadata\\IMetadataProvider' => __DIR__ . '/../../..' . '/lib/private/Metadata/IMetadataProvider.php', + 'OC\\Metadata\\MetadataManager' => __DIR__ . '/../../..' . '/lib/private/Metadata/MetadataManager.php', + 'OC\\Metadata\\Provider\\ExifProvider' => __DIR__ . '/../../..' . '/lib/private/Metadata/Provider/ExifProvider.php', 'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/ConsoleOutput.php', 'OC\\Migration\\SimpleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/SimpleOutput.php', diff --git a/lib/composer/composer/installed.php b/lib/composer/composer/installed.php index f12a8e00dbe..67a1ddfbd8c 100644 --- a/lib/composer/composer/installed.php +++ b/lib/composer/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../../', 'aliases' => array(), - 'reference' => '1225189f74d06606aafc4150d07584b90cea50dd', + 'reference' => '42c7886f80c7a5e767b192d07474114dd0848b16', 'name' => '__root__', 'dev' => false, ), @@ -16,7 +16,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../../', 'aliases' => array(), - 'reference' => '1225189f74d06606aafc4150d07584b90cea50dd', + 'reference' => '42c7886f80c7a5e767b192d07474114dd0848b16', 'dev_requirement' => false, ), ), diff --git a/lib/private/Files/ObjectStore/NoopScanner.php b/lib/private/Files/ObjectStore/NoopScanner.php index 42e212271d5..3b8cbdb18bb 100644 --- a/lib/private/Files/ObjectStore/NoopScanner.php +++ b/lib/private/Files/ObjectStore/NoopScanner.php @@ -31,7 +31,7 @@ use OC\Files\Storage\Storage; class NoopScanner extends Scanner { public function __construct(Storage $storage) { - //we don't need the storage, so do nothing here + // we don't need the storage, so do nothing here } /** diff --git a/lib/private/Metadata/Capabilities.php b/lib/private/Metadata/Capabilities.php new file mode 100644 index 00000000000..2fa0006f581 --- /dev/null +++ b/lib/private/Metadata/Capabilities.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> + * @license AGPL-3.0-or-later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Metadata; + +use OCP\Capabilities\IPublicCapability; +use OCP\IConfig; + +class Capabilities implements IPublicCapability { + private IMetadataManager $manager; + private IConfig $config; + + public function __construct(IMetadataManager $manager, IConfig $config) { + $this->manager = $manager; + $this->config = $config; + } + + public function getCapabilities() { + if ($this->config->getSystemValueBool('enable_file_metadata', true)) { + return ['metadataAvailable' => $this->manager->getCapabilities()]; + } + + return []; + } +} diff --git a/lib/private/Metadata/FileEventListener.php b/lib/private/Metadata/FileEventListener.php new file mode 100644 index 00000000000..fdec891c6e2 --- /dev/null +++ b/lib/private/Metadata/FileEventListener.php @@ -0,0 +1,84 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> + * @license AGPL-3.0-or-later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Metadata; + +use OC\Files\Filesystem; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\NodeDeletedEvent; +use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\Files\Events\NodeRemovedFromCache; +use OCP\Files\File; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Files\FileInfo; + +class FileEventListener implements IEventListener { + private IMetadataManager $manager; + + public function __construct(IMetadataManager $manager) { + $this->manager = $manager; + } + + private function shouldExtractMetadata(Node $node): bool { + try { + if ($node->getMimetype() === 'httpd/unix-directory') { + return false; + } + } catch (NotFoundException $e) { + return false; + } + if ($node->getSize(false) <= 0) { + return false; + } + + $path = $node->getPath(); + // TODO make this more dynamic, we have the same issue in other places + return !str_starts_with($path, 'appdata_') && !str_starts_with($path, 'files_versions/') && !str_starts_with($path, 'files_trashbin/'); + } + + public function handle(Event $event): void { + if ($event instanceof NodeRemovedFromCache) { + $view = Filesystem::getView(); + $info = $view->getFileInfo($event->getPath()); + if ($info && $info->getType() === FileInfo::TYPE_FILE) { + $this->manager->clearMetadata($info->getId()); + } + } + + if ($event instanceof NodeDeletedEvent) { + $node = $event->getNode(); + if ($this->shouldExtractMetadata($node)) { + /** @var File $node */ + $this->manager->clearMetadata($event->getNode()->getId()); + } + } + + if ($event instanceof NodeWrittenEvent) { + $node = $event->getNode(); + if ($this->shouldExtractMetadata($node)) { + /** @var File $node */ + $this->manager->generateMetadata($event->getNode(), false); + } + } + } +} diff --git a/lib/private/Metadata/FileMetadata.php b/lib/private/Metadata/FileMetadata.php new file mode 100644 index 00000000000..c53f5d7f619 --- /dev/null +++ b/lib/private/Metadata/FileMetadata.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Metadata; + +use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; + +/** + * @method string getGroupName() + * @method void setGroupName(string $groupName) + * @method string getMetadata() + * @method void setMetadata(array $metadata) + * @see OC\Core\Migrations\Version240000Date20220404230027 + */ +class FileMetadata extends Entity { + protected ?string $groupName = null; + protected ?array $metadata = null; + + public function __construct() { + $this->addType('groupName', 'string'); + $this->addType('metadata', Types::JSON); + } +} diff --git a/lib/private/Metadata/FileMetadataMapper.php b/lib/private/Metadata/FileMetadataMapper.php new file mode 100644 index 00000000000..53f750ae540 --- /dev/null +++ b/lib/private/Metadata/FileMetadataMapper.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> + * @license AGPL-3.0-or-later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Metadata; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class FileMetadataMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'file_metadata', FileMetadata::class); + } + + /** + * @return FileMetadata[] + * @throws Exception + */ + public function findForFile(int $fileId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + + return $this->findEntities($qb); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function findForGroupForFile(int $fileId, string $groupName): FileMetadata { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($qb->expr()->eq('group_name', $qb->createNamedParameter($groupName, IQueryBuilder::PARAM_STR))); + + return $this->findEntity($qb); + } + + /** + * @return array<int, FileMetadata> + * @throws Exception + */ + public function findForGroupForFiles(array $fileIds, string $groupName): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->in('id', $qb->createParameter('fileIds'))) + ->andWhere($qb->expr()->eq('group_name', $qb->createNamedParameter($groupName, IQueryBuilder::PARAM_STR))); + + $metadata = []; + foreach (array_chunk($fileIds, 1000) as $fileIdsChunk) { + $qb->setParameter('fileIds', $fileIdsChunk, IQueryBuilder::PARAM_INT_ARRAY); + /** @var FileMetadata[] $rawEntities */ + $rawEntities = $this->findEntities($qb); + foreach ($rawEntities as $entity) { + $metadata[$entity->getId()] = $entity; + } + } + + foreach ($fileIds as $id) { + if (isset($metadata[$id])) { + continue; + } + $empty = new FileMetadata(); + $empty->setMetadata([]); + $empty->setGroupName($groupName); + $empty->setId($id); + $metadata[$id] = $empty; + } + return $metadata; + } + + public function clear(int $fileId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + + $qb->executeStatement(); + } +} diff --git a/lib/private/Metadata/IMetadataManager.php b/lib/private/Metadata/IMetadataManager.php new file mode 100644 index 00000000000..d2d37f15c25 --- /dev/null +++ b/lib/private/Metadata/IMetadataManager.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace OC\Metadata; + +use OCP\Files\File; + +/** + * Interface to manage additional metadata for files + */ +interface IMetadataManager { + /** + * @param class-string<IMetadataProvider> $className + */ + public function registerProvider(string $className): void; + + /** + * Generate the metadata for one file + */ + public function generateMetadata(File $file, bool $checkExisting = false): void; + + /** + * Clear the metadata for one file + */ + public function clearMetadata(int $fileId): void; + + /** @return array<int, FileMetadata> */ + public function fetchMetadataFor(string $group, array $fileIds): array; + + /** + * Get the capabilites as an array of mimetype regex to the type provided + */ + public function getCapabilities(): array; +} diff --git a/lib/private/Metadata/IMetadataProvider.php b/lib/private/Metadata/IMetadataProvider.php new file mode 100644 index 00000000000..7cbe102a538 --- /dev/null +++ b/lib/private/Metadata/IMetadataProvider.php @@ -0,0 +1,41 @@ +<?php + +namespace OC\Metadata; + +use OCP\Files\File; + +/** + * Interface for the metadata providers. If you want an application to provide + * some metadata, you can use this to store them. + */ +interface IMetadataProvider { + /** + * The list of groups that this metadata provider is able to provide. + * + * @return string[] + */ + public static function groupsProvided(): array; + + /** + * Check if the metadata provider is available. A metadata provider might be + * unavailable due to a php extension not being installed. + */ + public static function isAvailable(): bool; + + /** + * Get the mimetypes supported as a regex. + */ + public static function getMimetypesSupported(): string; + + /** + * Execute the extraction on the specified file. The metadata should be + * grouped by metadata + * + * Each group should be json serializable and the string representation + * shouldn't be longer than 4000 characters. + * + * @param File $file The file to extract the metadata from + * @param array<string, FileMetadata> An array containing all the metadata fetched. + */ + public function execute(File $file): array; +} diff --git a/lib/private/Metadata/MetadataManager.php b/lib/private/Metadata/MetadataManager.php new file mode 100644 index 00000000000..d1cb896febf --- /dev/null +++ b/lib/private/Metadata/MetadataManager.php @@ -0,0 +1,105 @@ +<?php +/** + * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu> + * @license AGPL-3.0-or-later + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Metadata; + +use OC\Metadata\Provider\ExifProvider; +use OCP\Files\File; +use OCP\IConfig; +use Psr\Log\LoggerInterface; + +class MetadataManager implements IMetadataManager { + /** @var array<string, IMetadataProvider> */ + private array $providers; + private array $providerClasses; + private FileMetadataMapper $fileMetadataMapper; + private IConfig $config; + private LoggerInterface $logger; + + public function __construct( + FileMetadataMapper $fileMetadataMapper, + IConfig $config, + LoggerInterface $logger + ) { + $this->providers = []; + $this->providerClasses = []; + $this->fileMetadataMapper = $fileMetadataMapper; + $this->config = $config; + $this->logger = $logger; + + // TODO move to another place, where? + $this->registerProvider(ExifProvider::class); + } + + /** + * @param class-string<IMetadataProvider> $className + */ + public function registerProvider(string $className):void { + if (in_array($className, $this->providerClasses)) { + return; + } + + if (call_user_func([$className, 'isAvailable'])) { + $this->providers[call_user_func([$className, 'getMimetypesSupported'])] = \OC::$server->get($className); + } + } + + public function generateMetadata(File $file, bool $checkExisting = false): void { + $existingMetadataGroups = []; + + if ($checkExisting) { + $existingMetadata = $this->fileMetadataMapper->findForFile($file->getId()); + foreach ($existingMetadata as $metadata) { + $existingMetadataGroups[] = $metadata->getGroupName(); + } + } + + foreach ($this->providers as $supportedMimetype => $provider) { + if (preg_match($supportedMimetype, $file->getMimeType())) { + if (count(array_diff($provider::groupsProvided(), $existingMetadataGroups)) > 0) { + $metaDataGroup = $provider->execute($file); + foreach ($metaDataGroup as $group => $metadata) { + $this->fileMetadataMapper->insertOrUpdate($metadata); + } + } + } + } + } + + public function clearMetadata(int $fileId): void { + $this->fileMetadataMapper->clear($fileId); + } + + public function fetchMetadataFor(string $group, array $fileIds): array { + return $this->fileMetadataMapper->findForGroupForFiles($fileIds, $group); + } + + public function getCapabilities(): array { + $capabilities = []; + foreach ($this->providers as $supportedMimetype => $provider) { + foreach ($provider::groupsProvided() as $group) { + if (isset($capabilities[$group])) { + $capabilities[$group][] = $supportedMimetype; + } + $capabilities[$group] = [$supportedMimetype]; + } + } + return $capabilities; + } +} diff --git a/lib/private/Metadata/Provider/ExifProvider.php b/lib/private/Metadata/Provider/ExifProvider.php new file mode 100644 index 00000000000..2e1eb1d4208 --- /dev/null +++ b/lib/private/Metadata/Provider/ExifProvider.php @@ -0,0 +1,59 @@ +<?php + +namespace OC\Metadata\Provider; + +use OC\Metadata\FileMetadata; +use OC\Metadata\IMetadataProvider; +use OCP\Files\File; + +class ExifProvider implements IMetadataProvider { + public static function groupsProvided(): array { + return ['size']; + } + + public static function isAvailable(): bool { + return extension_loaded('exif'); + } + + public function execute(File $file): array { + $fileDescriptor = $file->fopen('rb'); + $data = @exif_read_data($fileDescriptor, 'ANY_TAG', true); + + $size = new FileMetadata(); + $size->setGroupName('size'); + $size->setId($file->getId()); + $size->setMetadata([]); + + if (!$data) { + $sizeResult = getimagesizefromstring($file->getContent()); + if ($sizeResult !== false) { + $size->setMetadata([ + 'width' => $sizeResult[0], + 'height' => $sizeResult[1], + ]); + } + + return [ + 'size' => $size, + ]; + } + + if (array_key_exists('COMPUTED', $data) + && array_key_exists('Width', $data['COMPUTED']) + && array_key_exists('Height', $data['COMPUTED']) + ) { + $size->setMetadata([ + 'width' => $data['COMPUTED']['Width'], + 'height' => $data['COMPUTED']['Height'], + ]); + } + + return [ + 'size' => $size, + ]; + } + + public static function getMimetypesSupported(): string { + return '/image\/.*/'; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 7817d1beafe..e9d673d3746 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -122,6 +122,9 @@ use OC\Log\PsrLoggerAdapter; use OC\Mail\Mailer; use OC\Memcache\ArrayCache; use OC\Memcache\Factory; +use OC\Metadata\Capabilities as MetadataCapabilities; +use OC\Metadata\IMetadataManager; +use OC\Metadata\MetadataManager; use OC\Notification\Manager; use OC\OCS\DiscoveryService; use OC\Preview\GeneratorHelper; @@ -151,7 +154,6 @@ use OC\Template\JSCombiner; use OCA\Theming\ImageManager; use OCA\Theming\ThemingDefaults; use OCA\Theming\Util; -use OCA\WorkflowEngine\Service\Logger; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\Authentication\LoginCredentials\IStore; @@ -241,15 +243,12 @@ use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; use OCP\Talk\IBroker; use OCP\User\Events\BeforePasswordUpdatedEvent; -use OCP\User\Events\BeforeUserCreatedEvent; -use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\BeforeUserLoggedInEvent; use OCP\User\Events\BeforeUserLoggedInWithCookieEvent; use OCP\User\Events\BeforeUserLoggedOutEvent; use OCP\User\Events\PasswordUpdatedEvent; use OCP\User\Events\PostLoginEvent; use OCP\User\Events\UserChangedEvent; -use OCP\User\Events\UserDeletedEvent; use OCP\User\Events\UserLoggedInEvent; use OCP\User\Events\UserLoggedInWithCookieEvent; use OCP\User\Events\UserLoggedOutEvent; @@ -1163,6 +1162,9 @@ class Server extends ServerContainer implements IServerContainer { $manager->registerCapability(function () use ($c) { return $c->get(\OC\Security\Bruteforce\Capabilities::class); }); + $manager->registerCapability(function () use ($c) { + return $c->get(MetadataCapabilities::class); + }); return $manager; }); /** @deprecated 19.0.0 */ @@ -1433,6 +1435,8 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(IBroker::class, Broker::class); + $this->registerAlias(IMetadataManager::class, MetadataManager::class); + $this->connectDispatcher(); } diff --git a/lib/public/AppFramework/Db/Entity.php b/lib/public/AppFramework/Db/Entity.php index 89e8f69859e..a059e3a27b0 100644 --- a/lib/public/AppFramework/Db/Entity.php +++ b/lib/public/AppFramework/Db/Entity.php @@ -120,6 +120,10 @@ abstract class Entity { if (!$args[0] instanceof \DateTime) { $args[0] = new \DateTime($args[0]); } + } elseif ($type === 'json') { + if (!is_array($args[0])) { + $args[0] = json_decode($args[0], true); + } } else { settype($args[0], $type); } diff --git a/lib/public/AppFramework/Db/QBMapper.php b/lib/public/AppFramework/Db/QBMapper.php index 5124650bc19..fa753a09dcf 100644 --- a/lib/public/AppFramework/Db/QBMapper.php +++ b/lib/public/AppFramework/Db/QBMapper.php @@ -253,6 +253,8 @@ abstract class QBMapper { return IQueryBuilder::PARAM_LOB; case 'datetime': return IQueryBuilder::PARAM_DATE; + case 'json': + return IQueryBuilder::PARAM_JSON; } return IQueryBuilder::PARAM_STR; diff --git a/lib/public/DB/QueryBuilder/IQueryBuilder.php b/lib/public/DB/QueryBuilder/IQueryBuilder.php index 76754f7bf41..afca9e372ee 100644 --- a/lib/public/DB/QueryBuilder/IQueryBuilder.php +++ b/lib/public/DB/QueryBuilder/IQueryBuilder.php @@ -65,6 +65,11 @@ interface IQueryBuilder { public const PARAM_DATE = 'datetime'; /** + * @since 24.0.0 + */ + public const PARAM_JSON = 'json'; + + /** * @since 9.0.0 */ public const PARAM_INT_ARRAY = Connection::PARAM_INT_ARRAY; diff --git a/lib/public/DB/Types.php b/lib/public/DB/Types.php index 4636ac3389f..31a474b03a0 100644 --- a/lib/public/DB/Types.php +++ b/lib/public/DB/Types.php @@ -110,4 +110,10 @@ final class Types { * @since 21.0.0 */ public const TIME = 'time'; + + /** + * @var string + * @since 24.0.0 + */ + public const JSON = 'json'; } |