Support getting and setting metadata in DAV requests

Signed-off-by: Louis Chemineau <louis@chmn.me>
pull/40964/head
Louis Chemineau 1 year ago
parent 9285fe04ff
commit d3a313f192
No known key found for this signature in database
  1. 4
      apps/dav/lib/Connector/Sabre/Directory.php
  2. 16
      apps/dav/lib/Connector/Sabre/File.php
  3. 127
      apps/dav/lib/Connector/Sabre/FilesPlugin.php
  4. 23
      apps/dav/lib/Files/FileSearchBackend.php
  5. 3
      apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php
  6. 7
      apps/files/lib/Command/Scan.php
  7. 4
      apps/files_trashbin/lib/Trashbin.php
  8. 17
      core/Application.php
  9. 8
      lib/composer/composer/autoload_classmap.php
  10. 8
      lib/composer/composer/autoload_static.php
  11. 11
      lib/private/Files/Cache/Cache.php
  12. 15
      lib/private/Files/Cache/CacheQueryBuilder.php
  13. 20
      lib/private/Files/Cache/QuerySearchHelper.php
  14. 15
      lib/private/FilesMetadata/FilesMetadataManager.php
  15. 2
      lib/private/FilesMetadata/Listener/MetadataUpdate.php
  16. 8
      lib/private/FilesMetadata/Model/FilesMetadata.php
  17. 2
      lib/private/FilesMetadata/Service/IndexRequestService.php
  18. 42
      lib/private/Metadata/Capabilities.php
  19. 108
      lib/private/Metadata/FileEventListener.php
  20. 51
      lib/private/Metadata/FileMetadata.php
  21. 177
      lib/private/Metadata/FileMetadataMapper.php
  22. 35
      lib/private/Metadata/IMetadataManager.php
  23. 41
      lib/private/Metadata/IMetadataProvider.php
  24. 95
      lib/private/Metadata/MetadataManager.php
  25. 139
      lib/private/Metadata/Provider/ExifProvider.php
  26. 8
      lib/private/Server.php
  27. 3
      lib/public/FilesMetadata/IFilesMetadataManager.php
  28. 4
      lib/public/FilesMetadata/Model/IFilesMetadata.php
  29. 87
      tests/lib/Metadata/FileMetadataMapperTest.php

@ -34,7 +34,6 @@ namespace OCA\DAV\Connector\Sabre;
use OC\Files\Mount\MoveableMount;
use OC\Files\View;
use OC\Metadata\FileMetadata;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
@ -70,9 +69,6 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
private ?array $quotaInfo = null;
private ?CachingTree $tree = null;
/** @var array<string, array<int, FileMetadata>> */
private array $metadata = [];
/**
* Sets up the node, expects a full path name
*/

@ -43,7 +43,6 @@ use OC\AppFramework\Http\Request;
use OC\Files\Filesystem;
use OC\Files\Stream\HashWrapper;
use OC\Files\View;
use OC\Metadata\FileMetadata;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Exception\BadGateway;
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
@ -81,9 +80,6 @@ class File extends Node implements IFile {
protected IRequest $request;
protected IL10N $l10n;
/** @var array<string, FileMetadata> */
private array $metadata = [];
/**
* Sets up the node, expects a full path name
*
@ -796,16 +792,4 @@ class File extends Node implements IFile {
public function getNode(): \OCP\Files\File {
return $this->node;
}
public function getMetadata(string $group): FileMetadata {
return $this->metadata[$group];
}
public function setMetadata(string $group, FileMetadata $metadata): void {
$this->metadata[$group] = $metadata;
}
public function hasMetadata(string $group) {
return array_key_exists($group, $this->metadata);
}
}

@ -35,19 +35,20 @@
namespace OCA\DAV\Connector\Sabre;
use OC\AppFramework\Http\Request;
use OC\Metadata\IMetadataManager;
use OC\FilesMetadata\Model\MetadataValueWrapper;
use OCP\Constants;
use OCP\Files\ForbiddenException;
use OCP\Files\StorageNotAvailableException;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IConfig;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Server;
@ -86,17 +87,6 @@ class FilesPlugin extends ServerPlugin {
public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
public const FILE_METADATA_SIZE = '{http://nextcloud.org/ns}file-metadata-size';
public const FILE_METADATA_GPS = '{http://nextcloud.org/ns}file-metadata-gps';
public const ALL_METADATA_PROPS = [
self::FILE_METADATA_SIZE => 'size',
self::FILE_METADATA_GPS => 'gps',
];
public const METADATA_MIMETYPES = [
'size' => 'image',
'gps' => 'image',
];
/** Reference to main server object */
private ?Server $server = null;
@ -434,31 +424,6 @@ class FilesPlugin extends ServerPlugin {
$propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getUploadTime();
});
if ($this->config->getSystemValueBool('enable_file_metadata', true)) {
foreach (self::ALL_METADATA_PROPS as $prop => $meta) {
$propFind->handle($prop, function () use ($node, $meta) {
if ($node->getFileInfo()->getMimePart() !== self::METADATA_MIMETYPES[$meta]) {
return [];
}
if ($node->hasMetadata($meta)) {
$metadata = $node->getMetadata($meta);
} else {
// This code path should not be called since we try to preload
// the metadata when loading the folder or the search results
// in one go
$metadataManager = \OC::$server->get(IMetadataManager::class);
$metadata = $metadataManager->fetchMetadataFor($meta, [$node->getId()])[$node->getId()];
// TODO would be nice to display this in the profiler...
\OC::$server->get(LoggerInterface::class)->debug('Inefficient fetching of metadata');
}
return $metadata->getValue();
});
}
}
}
if ($node instanceof Directory) {
@ -472,39 +437,6 @@ class FilesPlugin extends ServerPlugin {
$requestProperties = $propFind->getRequestedProperties();
$requestedMetaData = [];
foreach ($requestProperties as $requestProperty) {
if (isset(self::ALL_METADATA_PROPS[$requestProperty])) {
$requestedMetaData[] = self::ALL_METADATA_PROPS[$requestProperty];
}
}
if (
$this->config->getSystemValueBool('enable_file_metadata', true) &&
$propFind->getDepth() === 1 &&
$requestedMetaData
) {
$children = $node->getChildren();
// Preloading of the metadata
/** @var IMetaDataManager $metadataManager */
$metadataManager = \OC::$server->get(IMetadataManager::class);
foreach ($requestedMetaData as $requestedMeta) {
$relevantMimeType = self::METADATA_MIMETYPES[$requestedMeta];
$childrenForMeta = array_filter($children, function (INode $child) use ($relevantMimeType) {
return $child instanceof File && $child->getFileInfo()->getMimePart() === $relevantMimeType;
});
$fileIds = array_map(function (File $child) {
return $child->getFileInfo()->getId();
}, $childrenForMeta);
$preloadedMetadata = $metadataManager->fetchMetadataFor($requestedMeta, $fileIds);
foreach ($childrenForMeta as $child) {
$child->setMetadata($requestedMeta, $preloadedMetadata[$child->getFileInfo()->getId()]);
}
}
}
if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
|| in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) {
$nbFiles = 0;
@ -590,6 +522,57 @@ class FilesPlugin extends ServerPlugin {
$node->setCreationTime((int) $time);
return true;
});
/** @var IFilesMetadataManager */
$filesMetadataManager = \OCP\Server::get(IFilesMetadataManager::class);
$knownMetadata = $filesMetadataManager->getKnownMetadata();
foreach ($propPatch->getRemainingMutations() as $mutation) {
if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) {
continue;
}
$propPatch->handle($mutation, function (mixed $value) use ($knownMetadata, $node, $mutation, $filesMetadataManager): bool {
$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
// If the metadata is unknown, it defaults to string.
try {
$type = $knownMetadata->getType($metadataKey);
} catch (FilesMetadataNotFoundException) {
$type = IMetadataValueWrapper::TYPE_STRING;
}
switch ($type) {
case IMetadataValueWrapper::TYPE_STRING:
$metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
case IMetadataValueWrapper::TYPE_INT:
$metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
case IMetadataValueWrapper::TYPE_FLOAT:
$metadata->setFloat($metadataKey, $value);
break;
case IMetadataValueWrapper::TYPE_BOOL:
$metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
case IMetadataValueWrapper::TYPE_ARRAY:
$metadata->setArray($metadataKey, $value);
break;
case IMetadataValueWrapper::TYPE_STRING_LIST:
$metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
case IMetadataValueWrapper::TYPE_INT_LIST:
$metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey));
break;
}
$filesMetadataManager->saveMetadata($metadata);
return true;
});
}
/**
* Disable modification of the displayname property for files and
* folders via PROPPATCH. See PROPFIND for more information.

@ -31,7 +31,6 @@ use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchOrder;
use OC\Files\Search\SearchQuery;
use OC\Files\View;
use OC\Metadata\IMetadataManager;
use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\FilesPlugin;
@ -115,7 +114,6 @@ class FileSearchBackend implements ISearchBackend {
new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, true, false, false),
new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, true, false, false),
new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
new SearchPropertyDefinition(FilesPlugin::FILE_METADATA_SIZE, true, false, false, SearchPropertyDefinition::DATATYPE_STRING),
new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, true, false, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
];
@ -152,27 +150,6 @@ class FileSearchBackend implements ISearchBackend {
* @param string[] $requestProperties
*/
public function preloadPropertyFor(array $nodes, array $requestProperties): void {
if (in_array(FilesPlugin::FILE_METADATA_SIZE, $requestProperties, true)) {
// Preloading of the metadata
$fileIds = [];
foreach ($nodes as $node) {
/** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */
if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) {
/** @var \OCA\DAV\Connector\Sabre\File $node */
$fileIds[] = $node->getFileInfo()->getId();
}
}
/** @var IMetaDataManager $metadataManager */
$metadataManager = \OC::$server->get(IMetadataManager::class);
$preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds);
foreach ($nodes as $node) {
/** @var \OCP\Files\Node|\OCA\DAV\Connector\Sabre\Node $node */
if (str_starts_with($node->getFileInfo()->getMimeType(), 'image/')) {
/** @var \OCA\DAV\Connector\Sabre\File $node */
$node->setMetadata('size', $preloadedMetadata[$node->getFileInfo()->getId()]);
}
}
}
}
/**

@ -57,6 +57,8 @@ use Test\TestCase;
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*
* @group DB
*/
class FilesPluginTest extends TestCase {
public const GETETAG_PROPERTYNAME = FilesPlugin::GETETAG_PROPERTYNAME;
@ -424,6 +426,7 @@ class FilesPluginTest extends TestCase {
self::CREATIONDATE_PROPERTYNAME => $testCreationDate,
]);
$this->plugin->handleUpdateProperties(
'/dummypath',
$propPatch

@ -40,12 +40,10 @@ use OC\DB\Connection;
use OC\DB\ConnectionAdapter;
use OC\FilesMetadata\FilesMetadataManager;
use OC\ForbiddenException;
use OC\Metadata\MetadataManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\FileCacheUpdated;
use OCP\Files\Events\NodeAddedToCache;
use OCP\Files\Events\NodeRemovedFromCache;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
@ -71,7 +69,6 @@ class Scan extends Base {
public function __construct(
private IUserManager $userManager,
private IRootFolder $rootFolder,
private MetadataManager $metadataManager,
private FilesMetadataManager $filesMetadataManager,
private IEventDispatcher $eventDispatcher,
private LoggerInterface $logger,
@ -141,10 +138,6 @@ class Scan extends Base {
$this->abortIfInterrupted();
if ($scanMetadata) {
$node = $this->rootFolder->get($path);
if ($node instanceof File) {
$this->metadataManager->generateMetadata($node, false);
}
$this->filesMetadataManager->refreshMetadata(
$node,
IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND

@ -59,6 +59,7 @@ use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IConfig;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
@ -993,7 +994,8 @@ class Trashbin {
$query = new CacheQueryBuilder(
\OC::$server->getDatabaseConnection(),
\OC::$server->getSystemConfig(),
\OC::$server->get(LoggerInterface::class)
\OC::$server->get(LoggerInterface::class),
\OC::$server->get(IFilesMetadataManager::class),
);
$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
$parentId = $cache->getId($normalizedParentPath);

@ -44,7 +44,6 @@ use OC\Authentication\Listeners\UserDeletedWebAuthnCleanupListener;
use OC\Authentication\Notifications\Notifier as AuthenticationNotifier;
use OC\Core\Listener\BeforeTemplateRenderedListener;
use OC\Core\Notification\CoreNotifier;
use OC\Metadata\FileEventListener;
use OC\TagManager;
use OCP\AppFramework\App;
use OCP\AppFramework\Http\Events\BeforeLoginTemplateRenderedEvent;
@ -54,13 +53,9 @@ use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\DB\Events\AddMissingPrimaryKeyEvent;
use OCP\DB\Types;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\Files\Events\NodeRemovedFromCache;
use OCP\User\Events\BeforeUserDeletedEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
use OCP\IConfig;
/**
* Class Application
@ -331,18 +326,6 @@ class Application extends App {
$eventDispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedFilesCleanupListener::class);
$eventDispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedWebAuthnCleanupListener::class);
// Metadata
/** @var IConfig $config */
$config = $container->get(IConfig::class);
if ($config->getSystemValueBool('enable_file_metadata', true)) {
/** @psalm-suppress InvalidArgument */
$eventDispatcher->addServiceListener(NodeDeletedEvent::class, FileEventListener::class);
/** @psalm-suppress InvalidArgument */
$eventDispatcher->addServiceListener(NodeRemovedFromCache::class, FileEventListener::class);
/** @psalm-suppress InvalidArgument */
$eventDispatcher->addServiceListener(NodeWrittenEvent::class, FileEventListener::class);
}
// Tags
$eventDispatcher->addServiceListener(UserDeletedEvent::class, TagManager::class);
}

@ -1496,14 +1496,6 @@ return array(
'OC\\Memcache\\Redis' => $baseDir . '/lib/private/Memcache/Redis.php',
'OC\\Memcache\\WithLocalCache' => $baseDir . '/lib/private/Memcache/WithLocalCache.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',

@ -1529,14 +1529,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Memcache\\Redis' => __DIR__ . '/../../..' . '/lib/private/Memcache/Redis.php',
'OC\\Memcache\\WithLocalCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/WithLocalCache.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',

@ -59,6 +59,7 @@ use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchQuery;
use OCP\Files\Storage\IStorage;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IDBConnection;
use OCP\Util;
use Psr\Log\LoggerInterface;
@ -132,7 +133,8 @@ class Cache implements ICache {
return new CacheQueryBuilder(
$this->connection,
\OC::$server->getSystemConfig(),
\OC::$server->get(LoggerInterface::class)
\OC::$server->get(LoggerInterface::class),
\OC::$server->get(IFilesMetadataManager::class),
);
}
@ -154,6 +156,7 @@ class Cache implements ICache {
public function get($file) {
$query = $this->getQueryBuilder();
$query->selectFileCache();
$metadataQuery = $query->selectMetadata();
if (is_string($file) || $file == '') {
// normalize file
@ -175,6 +178,7 @@ class Cache implements ICache {
} elseif (!$data) {
return $data;
} else {
$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
return self::cacheEntryFromData($data, $this->mimetypeLoader);
}
}
@ -239,11 +243,14 @@ class Cache implements ICache {
->whereParent($fileId)
->orderBy('name', 'ASC');
$metadataQuery = $query->selectMetadata();
$result = $query->execute();
$files = $result->fetchAll();
$result->closeCursor();
return array_map(function (array $data) {
return array_map(function (array $data) use ($metadataQuery) {
$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
return self::cacheEntryFromData($data, $this->mimetypeLoader);
}, $files);
}

@ -29,6 +29,8 @@ namespace OC\Files\Cache;
use OC\DB\QueryBuilder\QueryBuilder;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\Model\IMetadataQuery;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
@ -38,7 +40,12 @@ use Psr\Log\LoggerInterface;
class CacheQueryBuilder extends QueryBuilder {
private ?string $alias = null;
public function __construct(IDBConnection $connection, SystemConfig $systemConfig, LoggerInterface $logger) {
public function __construct(
IDBConnection $connection,
SystemConfig $systemConfig,
LoggerInterface $logger,
private IFilesMetadataManager $filesMetadataManager,
) {
parent::__construct($connection, $systemConfig, $logger);
}
@ -127,4 +134,10 @@ class CacheQueryBuilder extends QueryBuilder {
return $this;
}
public function selectMetadata(): IMetadataQuery {
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($this, $this->alias, 'fileid');
$metadataQuery->retrieveMetadata();
return $metadataQuery;
}
}

@ -62,7 +62,8 @@ class QuerySearchHelper {
return new CacheQueryBuilder(
$this->connection,
$this->systemConfig,
$this->logger
$this->logger,
$this->filesMetadataManager,
);
}
@ -133,20 +134,6 @@ class QuerySearchHelper {
));
}
/**
* left join metadata and its indexes to the filecache table
*
* @param CacheQueryBuilder $query
*
* @return IMetadataQuery
*/
protected function equipQueryForMetadata(CacheQueryBuilder $query): IMetadataQuery {
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($query, 'file', 'fileid');
$metadataQuery->retrieveMetadata();
return $metadataQuery;
}
/**
* Perform a file system search in multiple caches
*
@ -186,7 +173,8 @@ class QuerySearchHelper {
$this->equipQueryForDavTags($query, $this->requireUser($searchQuery));
}
$metadataQuery = $this->equipQueryForMetadata($query);
$metadataQuery = $query->selectMetadata();
$this->applySearchConstraints($query, $searchQuery, $caches, $metadataQuery);
$result = $query->execute();

@ -38,7 +38,6 @@ use OCP\DB\Exception;
use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\Files\InvalidPathException;
@ -124,14 +123,23 @@ class FilesMetadataManager implements IFilesMetadataManager {
/**
* @param int $fileId file id
* @param boolean $generate Generate if metadata does not exists
*
* @inheritDoc
* @return IFilesMetadata
* @throws FilesMetadataNotFoundException if not found
* @since 28.0.0
*/
public function getMetadata(int $fileId): IFilesMetadata {
return $this->metadataRequestService->getMetadataFromFileId($fileId);
public function getMetadata(int $fileId, bool $generate = false): IFilesMetadata {
try {
return $this->metadataRequestService->getMetadataFromFileId($fileId);
} catch (FilesMetadataNotFoundException $ex) {
if ($generate) {
return new FilesMetadata($fileId);
}
throw $ex;
}
}
/**
@ -274,7 +282,6 @@ class FilesMetadataManager implements IFilesMetadataManager {
* @param IEventDispatcher $eventDispatcher
*/
public static function loadListeners(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addServiceListener(NodeCreatedEvent::class, MetadataUpdate::class);
$eventDispatcher->addServiceListener(NodeWrittenEvent::class, MetadataUpdate::class);
$eventDispatcher->addServiceListener(NodeDeletedEvent::class, MetadataDelete::class);
}

@ -51,7 +51,7 @@ class MetadataUpdate implements IEventListener {
* @param Event $event
*/
public function handle(Event $event): void {
if (!($event instanceof NodeCreatedEvent) && !($event instanceof NodeWrittenEvent)) {
if (!($event instanceof NodeWrittenEvent)) {
return;
}

@ -133,7 +133,7 @@ class FilesMetadata implements IFilesMetadata {
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function get(string $key): string {
public function getString(string $key): string {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
@ -276,10 +276,10 @@ class FilesMetadata implements IFilesMetadata {
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function set(string $key, string $value, bool $index = false): IFilesMetadata {
public function setString(string $key, string $value, bool $index = false): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->get($key) === $value && $index === $this->isIndex($key)) {
if ($this->getString($key) === $value && $index === $this->isIndex($key)) {
return $this; // we ignore if value and index have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
@ -506,7 +506,7 @@ class FilesMetadata implements IFilesMetadata {
return;
}
throw new FilesMetadataKeyFormatException('key can only contains alphanumerical characters, and dash (-)');
throw new FilesMetadataKeyFormatException('key can only contains alphanumerical characters, and dash (-, _)');
}
/**

@ -73,7 +73,7 @@ class IndexRequestService {
try {
$this->dropIndex($fileId, $key);
match ($metadataType) {
IMetadataValueWrapper::TYPE_STRING => $this->insertIndexString($fileId, $key, $filesMetadata->get($key)),
IMetadataValueWrapper::TYPE_STRING => $this->insertIndexString($fileId, $key, $filesMetadata->getString($key)),
IMetadataValueWrapper::TYPE_INT => $this->insertIndexInt($fileId, $key, $filesMetadata->getInt($key)),
IMetadataValueWrapper::TYPE_BOOL => $this->insertIndexBool($fileId, $key, $filesMetadata->getBool($key)),
IMetadataValueWrapper::TYPE_STRING_LIST => $this->insertIndexStringList($fileId, $key, $filesMetadata->getStringList($key)),

@ -1,42 +0,0 @@
<?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 {
public function __construct(
private IMetadataManager $manager,
private IConfig $config,
) {
}
public function getCapabilities(): array {
if ($this->config->getSystemValueBool('enable_file_metadata', true)) {
return ['metadataAvailable' => $this->manager->getCapabilities()];
}
return [];
}
}

@ -1,108 +0,0 @@
<?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;
use Psr\Log\LoggerInterface;
/**
* @template-implements IEventListener<NodeRemovedFromCache>
* @template-implements IEventListener<NodeDeletedEvent>
* @template-implements IEventListener<NodeWrittenEvent>
*/
class FileEventListener implements IEventListener {
public function __construct(
private IMetadataManager $manager,
private LoggerInterface $logger,
) {
}
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();
return $this->isCorrectPath($path);
}
private function isCorrectPath(string $path): bool {
// 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) {
if (!$this->isCorrectPath($event->getPath())) {
// Don't listen to paths for which we don't extract metadata
return;
}
$view = Filesystem::getView();
if (!$view) {
// Should not happen since a scan in the user folder should setup
// the file system.
$e = new \Exception(); // don't trigger, just get backtrace
$this->logger->error('Detecting deletion of a file with possible metadata but file system setup is not setup', [
'exception' => $e,
'app' => 'metadata'
]);
return;
}
$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);
}
}
}
}

@ -1,51 +0,0 @@
<?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 getValue()
* @method void setValue(string $value)
* @see \OC\Core\Migrations\Version240000Date20220404230027
*/
class FileMetadata extends Entity {
protected ?string $groupName = null;
protected ?string $value = null;
public function __construct() {
$this->addType('groupName', 'string');
$this->addType('value', Types::STRING);
}
public function getDecodedValue(): array {
return json_decode($this->getValue(), true) ?? [];
}
public function setArrayAsValue(array $value): void {
$this->setValue(json_encode($value, JSON_THROW_ON_ERROR));
}
}

@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
* @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\AppFramework\Db\Entity;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* @template-extends QBMapper<FileMetadata>
*/
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)))
->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->setValue('');
$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();
}
/**
* Updates an entry in the db from an entity
*
* @param FileMetadata $entity the entity that should be created
* @return FileMetadata the saved entity with the set id
* @throws Exception
* @throws \InvalidArgumentException if entity has no id
*/
public function update(Entity $entity): FileMetadata {
if (!($entity instanceof FileMetadata)) {
throw new \Exception("Entity should be a FileMetadata entity");
}
// entity needs an id
$id = $entity->getId();
if ($id === null) {
throw new \InvalidArgumentException('Entity which should be updated has no id');
}
// entity needs an group_name
$groupName = $entity->getGroupName();
if ($groupName === null) {
throw new \InvalidArgumentException('Entity which should be updated has no group_name');
}
$idType = $this->getParameterTypeForProperty($entity, 'id');
$groupNameType = $this->getParameterTypeForProperty($entity, 'groupName');
$value = $entity->getValue();
$valueType = $this->getParameterTypeForProperty($entity, 'value');
$qb = $this->db->getQueryBuilder();
$qb->update($this->tableName)
->set('value', $qb->createNamedParameter($value, $valueType))
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, $idType)))
->andWhere($qb->expr()->eq('group_name', $qb->createNamedParameter($groupName, $groupNameType)))
->executeStatement();
return $entity;
}
/**
* Override the insertOrUpdate as we could be in a transaction in which case we can not afford on error.
*
* @param FileMetadata $entity the entity that should be created/updated
* @return FileMetadata the saved entity with the (new) id
* @throws Exception
* @throws \InvalidArgumentException if entity has no id
*/
public function insertOrUpdate(Entity $entity): FileMetadata {
try {
$existingEntity = $this->findForGroupForFile($entity->getId(), $entity->getGroupName());
} catch (\Throwable) {
$existingEntity = null;
}
if ($existingEntity !== null) {
if ($entity->getValue() !== $existingEntity->getValue()) {
return $this->update($entity);
} else {
return $existingEntity;
}
} else {
return parent::insertOrUpdate($entity);
}
}
}

@ -1,35 +0,0 @@
<?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 capabilities as an array of mimetype regex to the type provided
*/
public function getCapabilities(): array;
}

@ -1,41 +0,0 @@
<?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;
}

@ -1,95 +0,0 @@
<?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;
class MetadataManager implements IMetadataManager {
/** @var array<string, IMetadataProvider> */
private array $providers = [];
private array $providerClasses = [];
public function __construct(
private FileMetadataMapper $fileMetadataMapper,
) {
// 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);
}
/**
* @return array<int, FileMetadata>
*/
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;
}
}

@ -1,139 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
* @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\Provider;
use OC\Metadata\FileMetadata;
use OC\Metadata\IMetadataProvider;
use OCP\Files\File;
use Psr\Log\LoggerInterface;
class ExifProvider implements IMetadataProvider {
public function __construct(
private LoggerInterface $logger,
) {
}
public static function groupsProvided(): array {
return ['size', 'gps'];
}
public static function isAvailable(): bool {
return extension_loaded('exif');
}
/** @return array{'gps'?: FileMetadata, 'size'?: FileMetadata} */
public function execute(File $file): array {
$exifData = [];
$fileDescriptor = $file->fopen('rb');
if ($fileDescriptor === false) {
return [];
}
$data = null;
try {
// Needed to make reading exif data reliable.
// This is to trigger this condition: https://github.com/php/php-src/blob/d64aa6f646a7b5e58359dc79479860164239580a/main/streams/streams.c#L710
// But I don't understand why 1 as a special meaning.
// Revert right after reading the exif data.
$oldBufferSize = stream_set_chunk_size($fileDescriptor, 1);
$data = @exif_read_data($fileDescriptor, 'ANY_TAG', true);
stream_set_chunk_size($fileDescriptor, $oldBufferSize);
} catch (\Exception $ex) {
$this->logger->info("Couldn't extract metadata for ".$file->getId(), ['exception' => $ex]);
}
$size = new FileMetadata();
$size->setGroupName('size');
$size->setId($file->getId());
$size->setArrayAsValue([]);
if (!$data) {
$sizeResult = getimagesizefromstring($file->getContent());
if ($sizeResult !== false) {
$size->setArrayAsValue([
'width' => $sizeResult[0],
'height' => $sizeResult[1],
]);
$exifData['size'] = $size;
}
} elseif (array_key_exists('COMPUTED', $data)) {
if (array_key_exists('Width', $data['COMPUTED']) && array_key_exists('Height', $data['COMPUTED'])) {
$size->setArrayAsValue([
'width' => $data['COMPUTED']['Width'],
'height' => $data['COMPUTED']['Height'],
]);
$exifData['size'] = $size;
}
}
if ($data && array_key_exists('GPS', $data)
&& array_key_exists('GPSLatitude', $data['GPS']) && array_key_exists('GPSLatitudeRef', $data['GPS'])
&& array_key_exists('GPSLongitude', $data['GPS']) && array_key_exists('GPSLongitudeRef', $data['GPS'])
) {
$gps = new FileMetadata();
$gps->setGroupName('gps');
$gps->setId($file->getId());
$gps->setArrayAsValue([
'latitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLatitude'], $data['GPS']['GPSLatitudeRef']),
'longitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLongitude'], $data['GPS']['GPSLongitudeRef']),
]);
$exifData['gps'] = $gps;
}
return $exifData;
}
public static function getMimetypesSupported(): string {
return '/image\/(png|jpeg|heif|webp|tiff)/';
}
/**
* @param array|string $coordinates
*/
private static function gpsDegreesToDecimal($coordinates, ?string $hemisphere): float {
if (is_string($coordinates)) {
$coordinates = array_map("trim", explode(",", $coordinates));
}
if (count($coordinates) !== 3) {
throw new \Exception('Invalid coordinate format: ' . json_encode($coordinates));
}
[$degrees, $minutes, $seconds] = array_map(function (string $rawDegree) {
$parts = explode('/', $rawDegree);
if ($parts[1] === '0') {
return 0;
}
return floatval($parts[0]) / floatval($parts[1] ?? 1);
}, $coordinates);
$sign = ($hemisphere === 'W' || $hemisphere === 'S') ? -1 : 1;
return $sign * ($degrees + $minutes / 60 + $seconds / 3600);
}
}

@ -121,9 +121,6 @@ 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\OCM\Model\OCMProvider;
use OC\OCM\OCMDiscoveryService;
@ -1136,9 +1133,6 @@ 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 */
@ -1415,8 +1409,6 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IBroker::class, Broker::class);
$this->registerAlias(IMetadataManager::class, MetadataManager::class);
$this->registerAlias(\OCP\Files\AppData\IAppDataFactory::class, \OC\Files\AppData\Factory::class);
$this->registerAlias(IBinaryFinder::class, BinaryFinder::class);

@ -69,12 +69,13 @@ interface IFilesMetadataManager {
* returns metadata from a file id
*
* @param int $fileId file id
* @param boolean $generate Generate if metadata does not exist
*
* @return IFilesMetadata
* @throws FilesMetadataNotFoundException if not found
* @since 28.0.0
*/
public function getMetadata(int $fileId): IFilesMetadata;
public function getMetadata(int $fileId, bool $generate = false): IFilesMetadata;
/**
* save metadata to database and refresh indexes.

@ -120,7 +120,7 @@ interface IFilesMetadata extends JsonSerializable {
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function get(string $key): string;
public function getString(string $key): string;
/**
* returns int value for a metadata key
@ -222,7 +222,7 @@ interface IFilesMetadata extends JsonSerializable {
* @return self
* @since 28.0.0
*/
public function set(string $key, string $value, bool $index = false): self;
public function setString(string $key, string $value, bool $index = false): self;
/**
* set a metadata key/value pair for int value

@ -1,87 +0,0 @@
<?php
declare(strict_types=1);
/**
* @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 Test\Metadata;
use OC\Metadata\FileMetadataMapper;
use OC\Metadata\FileMetadata;
use PHPUnit\Framework\MockObject\MockObject;
/**
* @group DB
* @package Test\DB\QueryBuilder
*/
class FileMetadataMapperTest extends \Test\TestCase {
/** @var IDBConnection */
protected $connection;
/** @var SystemConfig|MockObject */
protected $config;
/** @var FileMetadataMapper|MockObject */
protected $mapper;
protected function setUp(): void {
parent::setUp();
$this->connection = \OC::$server->getDatabaseConnection();
$this->mapper = new FileMetadataMapper($this->connection);
}
public function testFindForGroupForFiles() {
$file1 = new FileMetadata();
$file1->setId(1);
$file1->setGroupName('size');
$file1->setArrayAsValue([]);
$file2 = new FileMetadata();
$file2->setId(2);
$file2->setGroupName('size');
$file2->setArrayAsValue(['width' => 293, 'height' => 23]);
// not added, it's the default
$file3 = new FileMetadata();
$file3->setId(3);
$file3->setGroupName('size');
$file3->setArrayAsValue([]);
$file4 = new FileMetadata();
$file4->setId(4);
$file4->setGroupName('size');
$file4->setArrayAsValue(['complex' => ["yes", "maybe" => 34.0]]);
$this->mapper->insert($file1);
$this->mapper->insert($file2);
$this->mapper->insert($file4);
$files = $this->mapper->findForGroupForFiles([1, 2, 3, 4], 'size');
$this->assertEquals($files[1]->getValue(), $file1->getValue());
$this->assertEquals($files[2]->getValue(), $file2->getValue());
$this->assertEquals($files[3]->getDecodedValue(), $file3->getDecodedValue());
$this->assertEquals($files[4]->getValue(), $file4->getValue());
$this->mapper->clear(1);
$this->mapper->clear(2);
$this->mapper->clear(4);
}
}
Loading…
Cancel
Save