diff --git a/lib/private/Metadata/FileMetadataMapper.php b/lib/private/Metadata/FileMetadataMapper.php index 53f750ae54085..4d5591baf1feb 100644 --- a/lib/private/Metadata/FileMetadataMapper.php +++ b/lib/private/Metadata/FileMetadataMapper.php @@ -3,6 +3,7 @@ declare(strict_types=1); /** * @copyright Copyright 2022 Carl Schwan + * @copyright Copyright 2022 Louis Chmn * @license AGPL-3.0-or-later * * This code is free software: you can redistribute it and/or modify @@ -24,6 +25,7 @@ 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; @@ -102,4 +104,68 @@ public function clear(int $fileId): void { $qb->executeStatement(); } + + /** + * Updates an entry in the db from an entity + * + * @param Entity $entity the entity that should be created + * @return Entity the saved entity with the set id + * @throws Exception + * @throws \InvalidArgumentException if entity has no id + * @since 14.0.0 + */ + public function update(Entity $entity): Entity { + // if entity wasn't changed it makes no sense to run a db query + $properties = $entity->getUpdatedFields(); + if (\count($properties) === 0) { + return $entity; + } + + // entity needs an id + $id = $entity->getId(); + if ($id === null) { + throw new \InvalidArgumentException( + 'Entity which should be updated has no id'); + } + + if (!($entity instanceof FileMetadata)) { + throw new \Exception("Entity should be a FileMetadata entity"); + } + + // entity needs an group_name + $groupName = $entity->getGroupName(); + if ($id === null) { + throw new \InvalidArgumentException( + 'Entity which should be updated has no group_name'); + } + + // get updated fields to save, fields have to be set using a setter to + // be saved + // do not update the id and group_name field + unset($properties['id']); + unset($properties['group_name']); + + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName); + + // build the fields + foreach ($properties as $property => $updated) { + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + $value = $entity->$getter(); + + $type = $this->getParameterTypeForProperty($entity, $property); + $qb->set($column, $qb->createNamedParameter($value, $type)); + } + + $idType = $this->getParameterTypeForProperty($entity, 'id'); + $groupNameType = $this->getParameterTypeForProperty($entity, 'groupName'); + + $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($id, $idType))) + ->andWhere($qb->expr()->eq('group_name', $qb->createNamedParameter($groupName, $groupNameType))); + + $qb->executeStatement(); + + return $entity; + } } diff --git a/lib/private/Metadata/Provider/ExifProvider.php b/lib/private/Metadata/Provider/ExifProvider.php index 892671556b368..aa0b546468216 100644 --- a/lib/private/Metadata/Provider/ExifProvider.php +++ b/lib/private/Metadata/Provider/ExifProvider.php @@ -5,10 +5,19 @@ use OC\Metadata\FileMetadata; use OC\Metadata\IMetadataProvider; use OCP\Files\File; +use Psr\Log\LoggerInterface; class ExifProvider implements IMetadataProvider { + private LoggerInterface $logger; + + public function __construct( + LoggerInterface $logger + ) { + $this->logger = $logger; + } + public static function groupsProvided(): array { - return ['size']; + return ['size', 'gps']; } public static function isAvailable(): bool { @@ -16,8 +25,21 @@ public static function isAvailable(): bool { } public function execute(File $file): array { + $exifData = []; $fileDescriptor = $file->fopen('rb'); - $data = exif_read_data($fileDescriptor, 'COMPUTED', true); + + $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->warning("Couldn't extract metadata for ".$file->getId(), ['exception' => $ex]); + } $size = new FileMetadata(); $size->setGroupName('size'); @@ -31,29 +53,65 @@ public function execute(File $file): array { '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->setMetadata([ + 'width' => $data['COMPUTED']['Width'], + 'height' => $data['COMPUTED']['Height'], + ]); - return [ - 'size' => $size, - ]; + $exifData['size'] = $size; + } } - if (array_key_exists('COMPUTED', $data) - && array_key_exists('Width', $data['COMPUTED']) - && array_key_exists('Height', $data['COMPUTED']) + 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']) ) { - $size->setMetadata([ - 'width' => $data['COMPUTED']['Width'], - 'height' => $data['COMPUTED']['Height'], + $gps = new FileMetadata(); + $gps->setGroupName('gps'); + $gps->setId($file->getId()); + $gps->setMetadata([ + 'latitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLatitude'], $data['GPS']['GPSLatitudeRef']), + 'longitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLongitude'], $data['GPS']['GPSLongitudeRef']), ]); + + $exifData['gps'] = $gps; } - return [ - 'size' => $size, - ]; + return $exifData; } public static function getMimetypesSupported(): string { return '/image\/.*/'; } + + /** + * @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); + } }