Merge pull request #40964 from nextcloud/artonge/feat/metadata/port_providers
Support dynamic metadata request on PROPFIND requestspull/41266/head
commit
77c9550353
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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…
Reference in New Issue