Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Schwan <carl@carlschwan.eu>2022-04-05 00:15:00 +0300
committerCarl Schwan <carl@carlschwan.eu>2022-04-13 15:06:29 +0300
commit781784553889601d02553931aed8ff1fde95640b (patch)
tree21dd1b23c192d23be1ab1f468ff77165b7591172 /lib/private
parentcd95fce105fe5f0e71b1bcac7685464f936b9749 (diff)
Add a metadata service to store file metadata
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Diffstat (limited to 'lib/private')
-rw-r--r--lib/private/Files/ObjectStore/NoopScanner.php2
-rw-r--r--lib/private/Metadata/Capabilities.php44
-rw-r--r--lib/private/Metadata/FileEventListener.php84
-rw-r--r--lib/private/Metadata/FileMetadata.php43
-rw-r--r--lib/private/Metadata/FileMetadataMapper.php105
-rw-r--r--lib/private/Metadata/IMetadataManager.php35
-rw-r--r--lib/private/Metadata/IMetadataProvider.php41
-rw-r--r--lib/private/Metadata/MetadataManager.php100
-rw-r--r--lib/private/Metadata/Provider/ExifProvider.php51
-rw-r--r--lib/private/Server.php12
10 files changed, 512 insertions, 5 deletions
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..69e9cb3c852
--- /dev/null
+++ b/lib/private/Metadata/MetadataManager.php
@@ -0,0 +1,100 @@
+<?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) {
+ $capabilities[$supportedMimetype] = $provider::groupsProvided();
+ }
+ return $capabilities;
+ }
+}
diff --git a/lib/private/Metadata/Provider/ExifProvider.php b/lib/private/Metadata/Provider/ExifProvider.php
new file mode 100644
index 00000000000..91c858f6794
--- /dev/null
+++ b/lib/private/Metadata/Provider/ExifProvider.php
@@ -0,0 +1,51 @@
+<?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) {
+ 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();
}