From 0865eaa4fbf226b0eee0a9278909627a619adc25 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 5 Aug 2022 18:16:25 +0200 Subject: [PATCH 01/14] Allow managing face clusters through DAV Signed-off-by: Marcel Klehr --- appinfo/info.xml | 11 +++ lib/AppInfo/Application.php | 3 + lib/Dav/Faces/FacePhoto.php | 108 +++++++++++++++++++++++++ lib/Dav/Faces/FaceRoot.php | 112 ++++++++++++++++++++++++++ lib/Dav/Faces/FacesHome.php | 80 ++++++++++++++++++ lib/Dav/Faces/PropFindPlugin.php | 29 +++++++ lib/Dav/RecognizeHome.php | 78 ++++++++++++++++++ lib/Dav/RootCollection.php | 39 +++++++++ lib/Service/ClassifyImagesService.php | 0 lib/Service/FaceClusterAnalyzer.php | 2 +- 10 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 lib/Dav/Faces/FacePhoto.php create mode 100644 lib/Dav/Faces/FaceRoot.php create mode 100644 lib/Dav/Faces/FacesHome.php create mode 100644 lib/Dav/Faces/PropFindPlugin.php create mode 100644 lib/Dav/RecognizeHome.php create mode 100644 lib/Dav/RootCollection.php create mode 100644 lib/Service/ClassifyImagesService.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 50bd0cfc2..810eac8c6 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -75,5 +75,16 @@ The app does not send any sensitive data to cloud providers or similar services. + + + + + + OCA\Recognize\Dav\RootCollection + + + OCA\Recognize\Dav\Faces\PropFindPlugin + + diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 2eaa13160..f16217d67 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -8,6 +8,7 @@ namespace OCA\Recognize\AppInfo; +use OCA\DAV\Connector\Sabre\Principal; use OCA\Recognize\Hooks\FileListener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -32,6 +33,8 @@ public function __construct() { public function register(IRegistrationContext $context): void { @include_once __DIR__ . '/../../vendor/autoload.php'; + /** Register $principalBackend for the DAV collection */ + $context->registerServiceAlias('principalBackend', Principal::class); } /** diff --git a/lib/Dav/Faces/FacePhoto.php b/lib/Dav/Faces/FacePhoto.php new file mode 100644 index 000000000..b2241da6e --- /dev/null +++ b/lib/Dav/Faces/FacePhoto.php @@ -0,0 +1,108 @@ +detectionMapper = $detectionMapper; + $this->faceDetection = $faceDetection; + $this->userFolder = $userFolder; + } + + /** + * @inheritDoc + */ + public function getName() { + $file = $this->getFile(); + return $file->getId() . '-' . $file->getName(); + } + + /** + * @inheritDoc + * @throws \OCP\DB\Exception + */ + public function delete() { + $this->faceDetection->setClusterId(null); + $this->detectionMapper->update($this->faceDetection); + } + + /** + * @inheritDoc + */ + public function setName($name) { + throw new Forbidden('Cannot rename photos through faces API'); + } + + /** + * @inheritDoc + */ + public function put($data) { + throw new Forbidden('Can\'t write to photos trough the faces api'); + } + + public function getFile() : File { + $nodes = $this->userFolder->getById($this->faceDetection->getFileId()); + $node = current($nodes); + if ($node) { + if ($node instanceof File) { + return $node; + } else { + throw new NotFound("Photo is a folder"); + } + } else { + throw new NotFound("Photo not found for user"); + } + } + + public function getFaceDetection() : FaceDetection { + return $this->faceDetection; + } + + /** + * @inheritDoc + * @throws \Sabre\DAV\Exception\NotFound + */ + public function get() { + return $this->getFile()->fopen('r'); + } + + /** + * @inheritDoc + */ + public function getContentType() { + return $this->getFile()->getMimeType(); + } + + /** + * @inheritDoc + */ + public function getETag() { + return $this->getFile()->getEtag(); + } + + /** + * @inheritDoc + */ + public function getSize() { + return $this->getFile()->getSize(); + } + + /** + * @inheritDoc + */ + public function getLastModified() { + return $this->getFile()->getMTime(); + } +} diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php new file mode 100644 index 000000000..17dab283b --- /dev/null +++ b/lib/Dav/Faces/FaceRoot.php @@ -0,0 +1,112 @@ +clusterMapper = $clusterMapper; + $this->cluster = $cluster; + $this->user = $user; + $this->detectionMapper = $detectionMapper; + $this->rootFolder = $rootFolder; + } + + /** + * @inheritDoc + */ + public function getName() { + return $this->cluster->getTitle() !== '' ? $this->cluster->getTitle() : 'Person '.$this->cluster->getId(); + } + + /** + * @inheritDoc + */ + public function setName($name) { + $this->cluster->setTitle(basename($name)); + $this->clusterMapper->update($this->cluster); + } + + /** + * @inheritDoc + */ + public function createDirectory($name) { + throw new Forbidden('Not allowed to create directories in this folder'); + } + + /** + * @inheritDoc + */ + public function createFile($name, $data = null) { + throw new Forbidden('Not allowed to create files in this folder'); + } + + /** + * @throws \OCP\Files\NotPermittedException + * @throws \OC\User\NoUserException + */ + public function getChildren(): array { + return array_map(function (FaceDetection $detection) { + return new FacePhoto($this->detectionMapper, $detection, $this->rootFolder->getUserFolder($this->user->getUID())); + }, $this->detectionMapper->findByClusterId($this->cluster->getId())); + } + + public function getChild($name): FacePhoto { + foreach ($this->getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new NotFound("$name not found"); + } + + public function childExists($name): bool { + try { + $this->getChild($name); + return true; + } catch (NotFound $e) { + return false; + } + } + + public function moveInto($targetName, $sourcePath, INode $sourceNode) { + if ($sourceNode instanceof FacePhoto) { + $sourceNode->getFaceDetection()->setClusterId($this->cluster->getId()); + $this->detectionMapper->update($sourceNode->getFaceDetection()); + return true; + } + throw new Forbidden('Not a photo with a detected face, you can only move photos from the faces collection here'); + } + + /** + * @inheritDoc + * @throws \OCP\DB\Exception + */ + public function delete() { + $this->clusterMapper->delete($this->cluster); + } + + /** + * @inheritDoc + */ + public function getLastModified() : int { + return 0; + } +} diff --git a/lib/Dav/Faces/FacesHome.php b/lib/Dav/Faces/FacesHome.php new file mode 100644 index 000000000..a78673438 --- /dev/null +++ b/lib/Dav/Faces/FacesHome.php @@ -0,0 +1,80 @@ +faceClusterMapper = $faceClusterMapper; + $this->user = $user; + $this->faceDetectionMapper = $faceDetectionMapper; + $this->rootFolder = $rootFolder; + } + + public function delete() { + throw new Forbidden(); + } + + public function getName(): string { + return 'faces'; + } + + public function setName($name) { + throw new Forbidden('Permission denied to rename this folder'); + } + + public function createDirectory($name) { + throw new Forbidden('Not allowed to create directories in this folder'); + } + + public function createFile($name, $data = null) { + throw new Forbidden('Not allowed to create files in this folder'); + } + + public function getChild($name) { + foreach ($this->getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + + throw new NotFound(); + } + + /** + * @return \OCA\Recognize\Dav\Faces\FaceRoot[] + * @throws \OCP\DB\Exception + */ + public function getChildren(): array { + $clusters = $this->faceClusterMapper->findByUserId($this->user->getUID()); + return array_map(function (FaceCluster $cluster) { + return new FaceRoot($this->faceClusterMapper, $cluster, $this->user, $this->faceDetectionMapper, $this->rootFolder); + }, $clusters); + } + + public function childExists($name): bool { + try { + $this->getChild($name); + return true; + } catch (NotFound $e) { + return false; + } + } + + public function getLastModified(): int { + return 0; + } +} diff --git a/lib/Dav/Faces/PropFindPlugin.php b/lib/Dav/Faces/PropFindPlugin.php new file mode 100644 index 000000000..0b3651734 --- /dev/null +++ b/lib/Dav/Faces/PropFindPlugin.php @@ -0,0 +1,29 @@ +server = $server; + + $this->server->on('propFind', [$this, 'propFind']); + } + + + public function propFind(PropFind $propFind, INode $node) { + if (!($node instanceof FacePhoto)) { + return; + } + + $propFind->handle('{http://nextcloud.org/ns}file-name', function () use ($node) { + return $node->getFile()->getName(); + }); + } +} diff --git a/lib/Dav/RecognizeHome.php b/lib/Dav/RecognizeHome.php new file mode 100644 index 000000000..4ea4d6627 --- /dev/null +++ b/lib/Dav/RecognizeHome.php @@ -0,0 +1,78 @@ +principalInfo = $principalInfo; + $this->faceClusterMapper = $faceClusterMapper; + $this->user = $user; + $this->faceDetectionMapper = $faceDetectionMapper; + $this->rootFolder = $rootFolder; + } + + public function getName(): string { + [, $name] = \Sabre\Uri\split($this->principalInfo['uri']); + return $name; + } + + public function setName($name) { + throw new Forbidden('Permission denied to rename this folder'); + } + + public function delete() { + throw new Forbidden(); + } + + public function createFile($name, $data = null) { + throw new Forbidden('Not allowed to create files in this folder'); + } + + public function createDirectory($name) { + throw new Forbidden('Permission denied to create folders in this folder'); + } + + public function getChild($name) { + if ($name === 'faces') { + return new FacesHome($this->faceClusterMapper, $this->user, $this->faceDetectionMapper, $this->rootFolder); + } + + throw new NotFound(); + } + + /** + * @return FacesHome[] + */ + public function getChildren(): array { + return [new FacesHome($this->faceClusterMapper, $this->user, $this->faceDetectionMapper, $this->rootFolder)]; + } + + public function childExists($name): bool { + return $name === 'faces'; + } + + public function getLastModified(): int { + return 0; + } +} diff --git a/lib/Dav/RootCollection.php b/lib/Dav/RootCollection.php new file mode 100644 index 000000000..0998c9b56 --- /dev/null +++ b/lib/Dav/RootCollection.php @@ -0,0 +1,39 @@ +userSession = $userSession; + $this->faceClusterMapper = $faceClusterMapper; + $this->faceDetectionMapper = $faceDetectionMapper; + $this->rootFolder = $rootFolder; + } + + public function getChildForPrincipal(array $principalInfo): RecognizeHome { + [, $name] = \Sabre\Uri\split($principalInfo['uri']); + $user = $this->userSession->getUser(); + if (is_null($user) || $name !== $user->getUID()) { + throw new Forbidden(); + } + return new RecognizeHome($principalInfo, $this->faceClusterMapper, $user, $this->faceDetectionMapper, $this->rootFolder); + } + + public function getName() { + return 'recognize'; + } +} diff --git a/lib/Service/ClassifyImagesService.php b/lib/Service/ClassifyImagesService.php new file mode 100644 index 000000000..e69de29bb diff --git a/lib/Service/FaceClusterAnalyzer.php b/lib/Service/FaceClusterAnalyzer.php index c17ffec5c..39c0ca9d9 100644 --- a/lib/Service/FaceClusterAnalyzer.php +++ b/lib/Service/FaceClusterAnalyzer.php @@ -13,7 +13,7 @@ class FaceClusterAnalyzer { public const MIN_CLUSTER_DENSITY = 3; - public const MAX_INNER_CLUSTER_RADIUS = 0.35; + public const MAX_INNER_CLUSTER_RADIUS = 0.4; private FaceDetectionMapper $faceDetections; private FaceClusterMapper $faceClusters; From 4b63241f32a401ea63a86c47ba9655fd45179409 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 12 Aug 2022 16:35:43 +0200 Subject: [PATCH 02/14] PropFindPlugin: Add additional DAV properties Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FacePhoto.php | 37 ++++++++++++++++++++++++ lib/Dav/Faces/PropFindPlugin.php | 49 ++++++++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/lib/Dav/Faces/FacePhoto.php b/lib/Dav/Faces/FacePhoto.php index b2241da6e..96c7b5fea 100644 --- a/lib/Dav/Faces/FacePhoto.php +++ b/lib/Dav/Faces/FacePhoto.php @@ -15,6 +15,8 @@ class FacePhoto implements IFile { private FaceDetection $faceDetection; private Folder $userFolder; + public const TAG_FAVORITE = '_$!!$_'; + public function __construct(FaceDetectionMapper $detectionMapper, FaceDetection $faceDetection, Folder $userFolder) { $this->detectionMapper = $detectionMapper; $this->faceDetection = $faceDetection; @@ -105,4 +107,39 @@ public function getSize() { public function getLastModified() { return $this->getFile()->getMTime(); } + + + public function getFileInfo() { + $nodes = $this->userFolder->getById($this->getFile()->getId()); + $node = current($nodes); + if ($node) { + return $node->getFileInfo(); + } else { + throw new NotFound("Photo not found for user"); + } + } + + public function getMetadata(): array { + $metadataManager = \OCP\Server::get(\OC\Metadata\IMetadataManager::class); + $file = $this->getFile(); + $sizeMetadata = $metadataManager->fetchMetadataFor('size', [$file->getId()])[$file->getId()]; + return $sizeMetadata->getMetadata(); + } + + public function hasPreview(): bool { + $previewManager = \OCP\Server::get(\OCP\IPreview::class); + return $previewManager->isAvailable($this->getFileInfo()); + } + + public function isFavorite(): bool { + $tagManager = \OCP\Server::get(\OCP\ITagManager::class); + $tagger = $tagManager->load('files'); + $tags = $tagger->getTagsForObjects([$this->getFile()->getId()]); + + if ($tags === false || empty($tags)) { + return false; + } + + return array_search(self::TAG_FAVORITE, current($tags)) !== false; + } } diff --git a/lib/Dav/Faces/PropFindPlugin.php b/lib/Dav/Faces/PropFindPlugin.php index 0b3651734..b8669c00a 100644 --- a/lib/Dav/Faces/PropFindPlugin.php +++ b/lib/Dav/Faces/PropFindPlugin.php @@ -2,6 +2,10 @@ namespace OCA\Recognize\Dav\Faces; +use OCA\DAV\Connector\Sabre\File; +use OCA\Recognize\Db\FaceClusterMapper; +use OCA\Recognize\Db\FaceDetection; +use OCA\Recognize\Db\FaceDetectionMapper; use Sabre\DAV\INode; use Sabre\DAV\PropFind; use Sabre\DAV\Server; @@ -9,6 +13,18 @@ class PropFindPlugin extends ServerPlugin { private Server $server; + private FaceDetectionMapper $faceDetectionMapper; + private FaceClusterMapper $clusterMapper; + + public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; + public const FILE_METADATA_SIZE = '{http://nextcloud.org/ns}file-metadata-size'; + public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview'; + public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite'; + + public function __construct(FaceDetectionMapper $faceDetectionMapper, FaceClusterMapper $clusterMapper) { + $this->faceDetectionMapper = $faceDetectionMapper; + $this->clusterMapper = $clusterMapper; + } public function initialize(Server $server) { $this->server = $server; @@ -18,12 +34,35 @@ public function initialize(Server $server) { public function propFind(PropFind $propFind, INode $node) { - if (!($node instanceof FacePhoto)) { - return; + if ($node instanceof FacePhoto) { + $propFind->handle('{http://nextcloud.org/ns}file-name', function () use ($node) { + return $node->getFile()->getName(); + }); + + $propFind->handle('{http://nextcloud.org/ns}face-detections', function () use ($node) { + return json_encode(array_map(function (FaceDetection $face) { + $array = $face->toArray(); + $array['faceName'] = $this->clusterMapper->find($face->getClusterId())->getTitle() === ''? 'Person ' . $face->getClusterId() : $this->clusterMapper->find($face->getClusterId())->getTitle(); + return $array; + }, array_values(array_filter($this->faceDetectionMapper->findByFileId($node->getFile()->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); + }); + + $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, fn () => $node->getFile()->getId()); + $propFind->handle('{http://nextcloud.org/ns}file-name', fn () => $node->getFile()->getName()); + $propFind->handle('{http://nextcloud.org/ns}realpath', fn () => $node->getFileInfo()->getPath()); + $propFind->handle(self::FILE_METADATA_SIZE, fn () => json_encode($node->getMetadata())); + $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, fn () => json_encode($node->hasPreview())); + $propFind->handle(self::FAVORITE_PROPERTYNAME, fn () => $node->isFavorite() ? 1 : 0); } - $propFind->handle('{http://nextcloud.org/ns}file-name', function () use ($node) { - return $node->getFile()->getName(); - }); + if ($node instanceof File) { + $propFind->handle('{http://nextcloud.org/ns}face-detections', function () use ($node) { + return json_encode(array_map(function (FaceDetection $face) { + $array = $face->toArray(); + $array['faceName'] = $this->clusterMapper->find($face->getClusterId())->getTitle() === ''? 'Person ' . $face->getClusterId() : $this->clusterMapper->find($face->getClusterId())->getTitle(); + return $array; + }, array_values(array_filter($this->faceDetectionMapper->findByFileId($node->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); + }); + } } } From 3afde9ee86b7ec9ee3ca35a865fc63d8fab68c3b Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 17 Aug 2022 10:06:22 +0200 Subject: [PATCH 03/14] Performance optimizations Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FacePhoto.php | 47 +++++++++++++++++++++---------- lib/Dav/Faces/FaceRoot.php | 2 +- lib/Dav/Faces/FacesHome.php | 10 +++++-- lib/Dav/Faces/PropFindPlugin.php | 13 ++------- lib/Db/FaceDetection.php | 2 +- lib/Db/FaceDetectionMapper.php | 22 +++++++++++++++ lib/Db/FaceDetectionWithTitle.php | 19 +++++++++++++ 7 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 lib/Db/FaceDetectionWithTitle.php diff --git a/lib/Dav/Faces/FacePhoto.php b/lib/Dav/Faces/FacePhoto.php index 96c7b5fea..302d250d3 100644 --- a/lib/Dav/Faces/FacePhoto.php +++ b/lib/Dav/Faces/FacePhoto.php @@ -2,9 +2,11 @@ namespace OCA\Recognize\Dav\Faces; +use OCA\Recognize\Db\FaceCluster; use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; use OCP\Files\File; +use OCP\Files\FileInfo; use OCP\Files\Folder; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; @@ -13,12 +15,16 @@ class FacePhoto implements IFile { private FaceDetectionMapper $detectionMapper; private FaceDetection $faceDetection; + private FaceCluster $cluster; private Folder $userFolder; + private ?File $file = null; + private ?FileInfo $fileInfo = null; public const TAG_FAVORITE = '_$!!$_'; - public function __construct(FaceDetectionMapper $detectionMapper, FaceDetection $faceDetection, Folder $userFolder) { + public function __construct(FaceDetectionMapper $detectionMapper, FaceCluster $cluster, FaceDetection $faceDetection, Folder $userFolder) { $this->detectionMapper = $detectionMapper; + $this->cluster = $cluster; $this->faceDetection = $faceDetection; $this->userFolder = $userFolder; } @@ -54,17 +60,25 @@ public function put($data) { throw new Forbidden('Can\'t write to photos trough the faces api'); } + public function getCluster() : FaceCluster { + return $this->cluster; + } + public function getFile() : File { - $nodes = $this->userFolder->getById($this->faceDetection->getFileId()); - $node = current($nodes); - if ($node) { - if ($node instanceof File) { - return $node; + if ($this->file === null) { + $nodes = $this->userFolder->getById($this->faceDetection->getFileId()); + $node = current($nodes); + if ($node) { + if ($node instanceof File) { + return $this->file = $node; + } else { + throw new NotFound("Photo is a folder"); + } } else { - throw new NotFound("Photo is a folder"); + throw new NotFound("Photo not found for user"); } } else { - throw new NotFound("Photo not found for user"); + return $this->file; } } @@ -109,14 +123,17 @@ public function getLastModified() { } - public function getFileInfo() { - $nodes = $this->userFolder->getById($this->getFile()->getId()); - $node = current($nodes); - if ($node) { - return $node->getFileInfo(); - } else { - throw new NotFound("Photo not found for user"); + public function getFileInfo() : FileInfo { + if ($this->fileInfo === null) { + $nodes = $this->userFolder->getById($this->getFile()->getId()); + $node = current($nodes); + if ($node) { + return $this->fileInfo = $node->getFileInfo(); + } else { + throw new NotFound("Photo not found for user"); + } } + return $this->fileInfo; } public function getMetadata(): array { diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php index 17dab283b..af4a2af87 100644 --- a/lib/Dav/Faces/FaceRoot.php +++ b/lib/Dav/Faces/FaceRoot.php @@ -64,7 +64,7 @@ public function createFile($name, $data = null) { */ public function getChildren(): array { return array_map(function (FaceDetection $detection) { - return new FacePhoto($this->detectionMapper, $detection, $this->rootFolder->getUserFolder($this->user->getUID())); + return new FacePhoto($this->detectionMapper, $this->cluster, $detection, $this->rootFolder->getUserFolder($this->user->getUID())); }, $this->detectionMapper->findByClusterId($this->cluster->getId())); } diff --git a/lib/Dav/Faces/FacesHome.php b/lib/Dav/Faces/FacesHome.php index a78673438..003df6049 100644 --- a/lib/Dav/Faces/FacesHome.php +++ b/lib/Dav/Faces/FacesHome.php @@ -16,6 +16,7 @@ class FacesHome implements ICollection { private IUser $user; private FaceDetectionMapper $faceDetectionMapper; private IRootFolder $rootFolder; + private array $children = []; public function __construct(FaceClusterMapper $faceClusterMapper, IUser $user, FaceDetectionMapper $faceDetectionMapper, IRootFolder $rootFolder) { $this->faceClusterMapper = $faceClusterMapper; @@ -60,9 +61,12 @@ public function getChild($name) { */ public function getChildren(): array { $clusters = $this->faceClusterMapper->findByUserId($this->user->getUID()); - return array_map(function (FaceCluster $cluster) { - return new FaceRoot($this->faceClusterMapper, $cluster, $this->user, $this->faceDetectionMapper, $this->rootFolder); - }, $clusters); + if (count($this->children) === 0) { + $this->children = array_map(function (FaceCluster $cluster) { + return new FaceRoot($this->faceClusterMapper, $cluster, $this->user, $this->faceDetectionMapper, $this->rootFolder); + }, $clusters); + } + return $this->children; } public function childExists($name): bool { diff --git a/lib/Dav/Faces/PropFindPlugin.php b/lib/Dav/Faces/PropFindPlugin.php index b8669c00a..9fddab162 100644 --- a/lib/Dav/Faces/PropFindPlugin.php +++ b/lib/Dav/Faces/PropFindPlugin.php @@ -6,6 +6,7 @@ use OCA\Recognize\Db\FaceClusterMapper; use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; +use OCA\Recognize\Db\FaceDetectionWithTitle; use Sabre\DAV\INode; use Sabre\DAV\PropFind; use Sabre\DAV\Server; @@ -40,11 +41,7 @@ public function propFind(PropFind $propFind, INode $node) { }); $propFind->handle('{http://nextcloud.org/ns}face-detections', function () use ($node) { - return json_encode(array_map(function (FaceDetection $face) { - $array = $face->toArray(); - $array['faceName'] = $this->clusterMapper->find($face->getClusterId())->getTitle() === ''? 'Person ' . $face->getClusterId() : $this->clusterMapper->find($face->getClusterId())->getTitle(); - return $array; - }, array_values(array_filter($this->faceDetectionMapper->findByFileId($node->getFile()->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); + return json_encode(array_map(fn (FaceDetectionWithTitle $face) => $face->toArray(), array_values(array_filter($this->faceDetectionMapper->findByFileIdWithTitle($node->getFile()->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); }); $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, fn () => $node->getFile()->getId()); @@ -57,11 +54,7 @@ public function propFind(PropFind $propFind, INode $node) { if ($node instanceof File) { $propFind->handle('{http://nextcloud.org/ns}face-detections', function () use ($node) { - return json_encode(array_map(function (FaceDetection $face) { - $array = $face->toArray(); - $array['faceName'] = $this->clusterMapper->find($face->getClusterId())->getTitle() === ''? 'Person ' . $face->getClusterId() : $this->clusterMapper->find($face->getClusterId())->getTitle(); - return $array; - }, array_values(array_filter($this->faceDetectionMapper->findByFileId($node->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); + return json_encode(array_map(fn (FaceDetectionWithTitle $face) => $face->toArray(), array_values(array_filter($this->faceDetectionMapper->findByFileIdWithTitle($node->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); }); } } diff --git a/lib/Db/FaceDetection.php b/lib/Db/FaceDetection.php index ca832a748..0e8e4ce32 100644 --- a/lib/Db/FaceDetection.php +++ b/lib/Db/FaceDetection.php @@ -53,7 +53,7 @@ public function __construct() { public function toArray(): array { $array = []; - foreach (self::$fields as $field) { + foreach (static::$fields as $field) { $array[$field] = $this->{$field}; } return $array; diff --git a/lib/Db/FaceDetectionMapper.php b/lib/Db/FaceDetectionMapper.php index 6f05edd83..f608bf980 100644 --- a/lib/Db/FaceDetectionMapper.php +++ b/lib/Db/FaceDetectionMapper.php @@ -2,6 +2,7 @@ namespace OCA\Recognize\Db; +use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -67,4 +68,25 @@ public function assocWithCluster(FaceDetection $faceDetection, FaceCluster $face $faceDetection->setClusterId($faceCluster->getId()); $this->update($faceDetection); } + + public function findByFileIdWithTitle(int $fileId) { + $qb = $this->db->getQueryBuilder(); + $qb->select(array_merge(array_map(fn ($col) => 'd.'.$col, FaceDetection::$columns), ['c.title'])) + ->from('recognize_face_detections', 'd') + ->where($qb->expr()->eq('file_id', $qb->createPositionalParameter($fileId, IQueryBuilder::PARAM_INT))) + ->leftJoin('d', 'recognize_face_clusters', 'c', $qb->expr()->eq('d.cluster_id', 'c.id')); + return $this->findEntities($qb); + } + + protected function mapRowToEntity(array $row): Entity { + try { + return parent::mapRowToEntity($row); + } catch (\Exception $e) { + $entity = FaceDetectionWithTitle::fromRow($row); + if ($entity->getTitle() === '') { + $entity->setTitle('Person '.$entity->getClusterId()); + } + return $entity; + } + } } diff --git a/lib/Db/FaceDetectionWithTitle.php b/lib/Db/FaceDetectionWithTitle.php new file mode 100644 index 000000000..a0ccc89f6 --- /dev/null +++ b/lib/Db/FaceDetectionWithTitle.php @@ -0,0 +1,19 @@ +addType('title', 'string'); + } +} From f927125d334dbe18f39b0140f68c67f9311daa24 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 18 Aug 2022 13:51:30 +0200 Subject: [PATCH 04/14] Default cluster title: Just the id Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FaceRoot.php | 2 +- lib/Db/FaceDetectionMapper.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php index af4a2af87..9f9a3889a 100644 --- a/lib/Dav/Faces/FaceRoot.php +++ b/lib/Dav/Faces/FaceRoot.php @@ -33,7 +33,7 @@ public function __construct(FaceClusterMapper $clusterMapper, FaceCluster $clust * @inheritDoc */ public function getName() { - return $this->cluster->getTitle() !== '' ? $this->cluster->getTitle() : 'Person '.$this->cluster->getId(); + return $this->cluster->getTitle() !== '' ? $this->cluster->getTitle() : ''.$this->cluster->getId(); } /** diff --git a/lib/Db/FaceDetectionMapper.php b/lib/Db/FaceDetectionMapper.php index f608bf980..4c1bdac9b 100644 --- a/lib/Db/FaceDetectionMapper.php +++ b/lib/Db/FaceDetectionMapper.php @@ -84,7 +84,7 @@ protected function mapRowToEntity(array $row): Entity { } catch (\Exception $e) { $entity = FaceDetectionWithTitle::fromRow($row); if ($entity->getTitle() === '') { - $entity->setTitle('Person '.$entity->getClusterId()); + $entity->setTitle($entity->getClusterId()); } return $entity; } From ffab182f7fec3ea49c4008fa746388a3259c53cf Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 18 Aug 2022 17:54:26 +0200 Subject: [PATCH 05/14] Do not query Fileinfo separately Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FacePhoto.php | 18 +----------------- lib/Dav/Faces/PropFindPlugin.php | 2 +- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/lib/Dav/Faces/FacePhoto.php b/lib/Dav/Faces/FacePhoto.php index 302d250d3..86f0788be 100644 --- a/lib/Dav/Faces/FacePhoto.php +++ b/lib/Dav/Faces/FacePhoto.php @@ -6,7 +6,6 @@ use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; use OCP\Files\File; -use OCP\Files\FileInfo; use OCP\Files\Folder; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; @@ -18,7 +17,6 @@ class FacePhoto implements IFile { private FaceCluster $cluster; private Folder $userFolder; private ?File $file = null; - private ?FileInfo $fileInfo = null; public const TAG_FAVORITE = '_$!!$_'; @@ -122,20 +120,6 @@ public function getLastModified() { return $this->getFile()->getMTime(); } - - public function getFileInfo() : FileInfo { - if ($this->fileInfo === null) { - $nodes = $this->userFolder->getById($this->getFile()->getId()); - $node = current($nodes); - if ($node) { - return $this->fileInfo = $node->getFileInfo(); - } else { - throw new NotFound("Photo not found for user"); - } - } - return $this->fileInfo; - } - public function getMetadata(): array { $metadataManager = \OCP\Server::get(\OC\Metadata\IMetadataManager::class); $file = $this->getFile(); @@ -145,7 +129,7 @@ public function getMetadata(): array { public function hasPreview(): bool { $previewManager = \OCP\Server::get(\OCP\IPreview::class); - return $previewManager->isAvailable($this->getFileInfo()); + return $previewManager->isAvailable($this->getFile()); } public function isFavorite(): bool { diff --git a/lib/Dav/Faces/PropFindPlugin.php b/lib/Dav/Faces/PropFindPlugin.php index 9fddab162..eaa7368fe 100644 --- a/lib/Dav/Faces/PropFindPlugin.php +++ b/lib/Dav/Faces/PropFindPlugin.php @@ -46,7 +46,7 @@ public function propFind(PropFind $propFind, INode $node) { $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, fn () => $node->getFile()->getId()); $propFind->handle('{http://nextcloud.org/ns}file-name', fn () => $node->getFile()->getName()); - $propFind->handle('{http://nextcloud.org/ns}realpath', fn () => $node->getFileInfo()->getPath()); + $propFind->handle('{http://nextcloud.org/ns}realpath', fn () => $node->getFile()->getPath()); $propFind->handle(self::FILE_METADATA_SIZE, fn () => json_encode($node->getMetadata())); $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, fn () => json_encode($node->hasPreview())); $propFind->handle(self::FAVORITE_PROPERTYNAME, fn () => $node->isFavorite() ? 1 : 0); From 52095babd8e1bbecfd6903405f4955b4979a66a0 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 19 Aug 2022 10:00:54 +0200 Subject: [PATCH 06/14] Properly inject classes Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FacePhoto.php | 27 ++++++++++++++++----------- lib/Dav/Faces/FaceRoot.php | 13 +++++++++++-- lib/Dav/Faces/FacesHome.php | 13 +++++++++++-- lib/Dav/RecognizeHome.php | 25 ++++++++++++++++--------- lib/Dav/RootCollection.php | 13 +++++++++++-- 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/lib/Dav/Faces/FacePhoto.php b/lib/Dav/Faces/FacePhoto.php index 86f0788be..2c5eefd86 100644 --- a/lib/Dav/Faces/FacePhoto.php +++ b/lib/Dav/Faces/FacePhoto.php @@ -2,11 +2,15 @@ namespace OCA\Recognize\Dav\Faces; +use OC\Metadata\IMetadataManager; use OCA\Recognize\Db\FaceCluster; use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; use OCP\Files\File; use OCP\Files\Folder; +use OCP\IPreview; +use OCP\ITagManager; +use OCP\ITags; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\IFile; @@ -17,14 +21,18 @@ class FacePhoto implements IFile { private FaceCluster $cluster; private Folder $userFolder; private ?File $file = null; + private ITagManager $tagManager; + private IMetadataManager $metadataManager; + private IPreview $preview; - public const TAG_FAVORITE = '_$!!$_'; - - public function __construct(FaceDetectionMapper $detectionMapper, FaceCluster $cluster, FaceDetection $faceDetection, Folder $userFolder) { + public function __construct(FaceDetectionMapper $detectionMapper, FaceCluster $cluster, FaceDetection $faceDetection, Folder $userFolder, ITagManager $tagManager, IMetadataManager $metadataManager, IPreview $preview) { $this->detectionMapper = $detectionMapper; $this->cluster = $cluster; $this->faceDetection = $faceDetection; $this->userFolder = $userFolder; + $this->tagManager = $tagManager; + $this->metadataManager = $metadataManager; + $this->preview = $preview; } /** @@ -120,27 +128,24 @@ public function getLastModified() { return $this->getFile()->getMTime(); } - public function getMetadata(): array { - $metadataManager = \OCP\Server::get(\OC\Metadata\IMetadataManager::class); + public function getMetadata(): string { $file = $this->getFile(); - $sizeMetadata = $metadataManager->fetchMetadataFor('size', [$file->getId()])[$file->getId()]; + $sizeMetadata = $this->metadataManager->fetchMetadataFor('size', [$file->getId()])[$file->getId()]; return $sizeMetadata->getMetadata(); } public function hasPreview(): bool { - $previewManager = \OCP\Server::get(\OCP\IPreview::class); - return $previewManager->isAvailable($this->getFile()); + return $this->preview->isAvailable($this->getFile()); } public function isFavorite(): bool { - $tagManager = \OCP\Server::get(\OCP\ITagManager::class); - $tagger = $tagManager->load('files'); + $tagger = $this->tagManager->load('files'); $tags = $tagger->getTagsForObjects([$this->getFile()->getId()]); if ($tags === false || empty($tags)) { return false; } - return array_search(self::TAG_FAVORITE, current($tags)) !== false; + return array_search(ITags::TAG_FAVORITE, current($tags)) !== false; } } diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php index 9f9a3889a..8ed00d73f 100644 --- a/lib/Dav/Faces/FaceRoot.php +++ b/lib/Dav/Faces/FaceRoot.php @@ -2,11 +2,14 @@ namespace OCA\Recognize\Dav\Faces; +use OC\Metadata\IMetadataManager; use OCA\Recognize\Db\FaceCluster; use OCA\Recognize\Db\FaceClusterMapper; use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; use OCP\Files\IRootFolder; +use OCP\IPreview; +use OCP\ITagManager; use OCP\IUser; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; @@ -20,13 +23,19 @@ class FaceRoot implements ICollection, IMoveTarget { private IUser $user; private FaceDetectionMapper $detectionMapper; private IRootFolder $rootFolder; + private IMetadataManager $metadataManager; + private ITagManager $tagManager; + private IPreview $previewManager; - public function __construct(FaceClusterMapper $clusterMapper, FaceCluster $cluster, IUser $user, FaceDetectionMapper $detectionMapper, IRootFolder $rootFolder) { + public function __construct(FaceClusterMapper $clusterMapper, FaceCluster $cluster, IUser $user, FaceDetectionMapper $detectionMapper, IRootFolder $rootFolder, IMetadataManager $metadataManager, ITagManager $tagManager, IPreview $previewManager) { $this->clusterMapper = $clusterMapper; $this->cluster = $cluster; $this->user = $user; $this->detectionMapper = $detectionMapper; $this->rootFolder = $rootFolder; + $this->metadataManager = $metadataManager; + $this->tagManager = $tagManager; + $this->previewManager = $previewManager; } /** @@ -64,7 +73,7 @@ public function createFile($name, $data = null) { */ public function getChildren(): array { return array_map(function (FaceDetection $detection) { - return new FacePhoto($this->detectionMapper, $this->cluster, $detection, $this->rootFolder->getUserFolder($this->user->getUID())); + return new FacePhoto($this->detectionMapper, $this->cluster, $detection, $this->rootFolder->getUserFolder($this->user->getUID()), $this->tagManager, $this->metadataManager, $this->previewManager); }, $this->detectionMapper->findByClusterId($this->cluster->getId())); } diff --git a/lib/Dav/Faces/FacesHome.php b/lib/Dav/Faces/FacesHome.php index 003df6049..57c48b7bd 100644 --- a/lib/Dav/Faces/FacesHome.php +++ b/lib/Dav/Faces/FacesHome.php @@ -2,10 +2,13 @@ namespace OCA\Recognize\Dav\Faces; +use OC\Metadata\IMetadataManager; use OCA\Recognize\Db\FaceCluster; use OCA\Recognize\Db\FaceClusterMapper; use OCA\Recognize\Db\FaceDetectionMapper; use OCP\Files\IRootFolder; +use OCP\IPreview; +use OCP\ITagManager; use OCP\IUser; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; @@ -17,12 +20,18 @@ class FacesHome implements ICollection { private FaceDetectionMapper $faceDetectionMapper; private IRootFolder $rootFolder; private array $children = []; + private ITagManager $tagManager; + private IMetadataManager $metadataManager; + private IPreview $previewManager; - public function __construct(FaceClusterMapper $faceClusterMapper, IUser $user, FaceDetectionMapper $faceDetectionMapper, IRootFolder $rootFolder) { + public function __construct(FaceClusterMapper $faceClusterMapper, IUser $user, FaceDetectionMapper $faceDetectionMapper, IRootFolder $rootFolder, ITagManager $tagManager, IMetadataManager $metadataManager, IPreview $previewManager) { $this->faceClusterMapper = $faceClusterMapper; $this->user = $user; $this->faceDetectionMapper = $faceDetectionMapper; $this->rootFolder = $rootFolder; + $this->tagManager = $tagManager; + $this->metadataManager = $metadataManager; + $this->previewManager = $previewManager; } public function delete() { @@ -63,7 +72,7 @@ public function getChildren(): array { $clusters = $this->faceClusterMapper->findByUserId($this->user->getUID()); if (count($this->children) === 0) { $this->children = array_map(function (FaceCluster $cluster) { - return new FaceRoot($this->faceClusterMapper, $cluster, $this->user, $this->faceDetectionMapper, $this->rootFolder); + return new FaceRoot($this->faceClusterMapper, $cluster, $this->user, $this->faceDetectionMapper, $this->rootFolder, $this->metadataManager, $this->tagManager, $this->previewManager); }, $clusters); } return $this->children; diff --git a/lib/Dav/RecognizeHome.php b/lib/Dav/RecognizeHome.php index 4ea4d6627..d2b94244a 100644 --- a/lib/Dav/RecognizeHome.php +++ b/lib/Dav/RecognizeHome.php @@ -2,10 +2,13 @@ namespace OCA\Recognize\Dav; +use OC\Metadata\IMetadataManager; use OCA\Recognize\Dav\Faces\FacesHome; use OCA\Recognize\Db\FaceClusterMapper; use OCA\Recognize\Db\FaceDetectionMapper; use OCP\Files\IRootFolder; +use OCP\IPreview; +use OCP\ITagManager; use OCP\IUser; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; @@ -15,21 +18,21 @@ class RecognizeHome implements ICollection { private array $principalInfo; private FaceClusterMapper $faceClusterMapper; private IUser $user; - /** - * @var \OCA\Recognize\Db\FaceDetectionMapper - */ private FaceDetectionMapper $faceDetectionMapper; - /** - * @var \OCP\Files\IRootFolder - */ private IRootFolder $rootFolder; + private ITagManager $tagManager; + private IMetadataManager $metadataManager; + private IPreview $previewManager; - public function __construct(array $principalInfo, FaceClusterMapper $faceClusterMapper, IUser $user, FaceDetectionMapper $faceDetectionMapper, IRootFolder $rootFolder) { + public function __construct(array $principalInfo, FaceClusterMapper $faceClusterMapper, IUser $user, FaceDetectionMapper $faceDetectionMapper, IRootFolder $rootFolder, ITagManager $tagManager, IMetadataManager $metadataManager, IPreview $previewManager) { $this->principalInfo = $principalInfo; $this->faceClusterMapper = $faceClusterMapper; $this->user = $user; $this->faceDetectionMapper = $faceDetectionMapper; $this->rootFolder = $rootFolder; + $this->tagManager = $tagManager; + $this->metadataManager = $metadataManager; + $this->previewManager = $previewManager; } public function getName(): string { @@ -53,9 +56,13 @@ public function createDirectory($name) { throw new Forbidden('Permission denied to create folders in this folder'); } + private function getFacesHome() { + return new FacesHome($this->faceClusterMapper, $this->user, $this->faceDetectionMapper, $this->rootFolder, $this->tagManager, $this->metadataManager, $this->previewManager); + } + public function getChild($name) { if ($name === 'faces') { - return new FacesHome($this->faceClusterMapper, $this->user, $this->faceDetectionMapper, $this->rootFolder); + return $this->getFacesHome(); } throw new NotFound(); @@ -65,7 +72,7 @@ public function getChild($name) { * @return FacesHome[] */ public function getChildren(): array { - return [new FacesHome($this->faceClusterMapper, $this->user, $this->faceDetectionMapper, $this->rootFolder)]; + return [$this->getFacesHome()]; } public function childExists($name): bool { diff --git a/lib/Dav/RootCollection.php b/lib/Dav/RootCollection.php index 0998c9b56..b55f72875 100644 --- a/lib/Dav/RootCollection.php +++ b/lib/Dav/RootCollection.php @@ -2,9 +2,12 @@ namespace OCA\Recognize\Dav; +use OC\Metadata\IMetadataManager; use OCA\Recognize\Db\FaceClusterMapper; use OCA\Recognize\Db\FaceDetectionMapper; use OCP\Files\IRootFolder; +use OCP\IPreview; +use OCP\ITagManager; use OCP\IUserSession; use Sabre\DAV\Exception\Forbidden; use \Sabre\DAVACL\AbstractPrincipalCollection; @@ -15,13 +18,19 @@ class RootCollection extends AbstractPrincipalCollection { private FaceClusterMapper $faceClusterMapper; private FaceDetectionMapper $faceDetectionMapper; private IRootFolder $rootFolder; + private ITagManager $tagManager; + private IMetadataManager $metadataManager; + private IPreview $previewManager; - public function __construct(BackendInterface $principalBackend, IUserSession $userSession, FaceClusterMapper $faceClusterMapper, FaceDetectionMapper $faceDetectionMapper, IRootFolder $rootFolder) { + public function __construct(BackendInterface $principalBackend, IUserSession $userSession, FaceClusterMapper $faceClusterMapper, FaceDetectionMapper $faceDetectionMapper, IRootFolder $rootFolder, ITagManager $tagManager, IMetadataManager $metadataManager, IPreview $previewManager) { parent::__construct($principalBackend, 'principals/users'); $this->userSession = $userSession; $this->faceClusterMapper = $faceClusterMapper; $this->faceDetectionMapper = $faceDetectionMapper; $this->rootFolder = $rootFolder; + $this->tagManager = $tagManager; + $this->metadataManager = $metadataManager; + $this->previewManager = $previewManager; } public function getChildForPrincipal(array $principalInfo): RecognizeHome { @@ -30,7 +39,7 @@ public function getChildForPrincipal(array $principalInfo): RecognizeHome { if (is_null($user) || $name !== $user->getUID()) { throw new Forbidden(); } - return new RecognizeHome($principalInfo, $this->faceClusterMapper, $user, $this->faceDetectionMapper, $this->rootFolder); + return new RecognizeHome($principalInfo, $this->faceClusterMapper, $user, $this->faceDetectionMapper, $this->rootFolder, $this->tagManager, $this->metadataManager, $this->previewManager); } public function getName() { From e489defae082cceef192eaab88caace2efea4e1f Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 19 Aug 2022 10:33:15 +0200 Subject: [PATCH 07/14] PropFindPlugin: Use existing constants Signed-off-by: Marcel Klehr --- lib/Dav/Faces/PropFindPlugin.php | 34 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/lib/Dav/Faces/PropFindPlugin.php b/lib/Dav/Faces/PropFindPlugin.php index eaa7368fe..7a95cb200 100644 --- a/lib/Dav/Faces/PropFindPlugin.php +++ b/lib/Dav/Faces/PropFindPlugin.php @@ -3,7 +3,8 @@ namespace OCA\Recognize\Dav\Faces; use OCA\DAV\Connector\Sabre\File; -use OCA\Recognize\Db\FaceClusterMapper; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\TagsPlugin; use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; use OCA\Recognize\Db\FaceDetectionWithTitle; @@ -13,18 +14,15 @@ use Sabre\DAV\ServerPlugin; class PropFindPlugin extends ServerPlugin { + public const FACE_DETECTIONS_PROPERTYNAME = '{http://nextcloud.org/ns}face-detections'; + public const FILE_NAME_PROPERTYNAME = '{http://nextcloud.org/ns}file-name'; + public const REALPATH_PROPERTYNAME = '{http://nextcloud.org/ns}realpath'; + private Server $server; private FaceDetectionMapper $faceDetectionMapper; - private FaceClusterMapper $clusterMapper; - - public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; - public const FILE_METADATA_SIZE = '{http://nextcloud.org/ns}file-metadata-size'; - public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview'; - public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite'; - public function __construct(FaceDetectionMapper $faceDetectionMapper, FaceClusterMapper $clusterMapper) { + public function __construct(FaceDetectionMapper $faceDetectionMapper) { $this->faceDetectionMapper = $faceDetectionMapper; - $this->clusterMapper = $clusterMapper; } public function initialize(Server $server) { @@ -36,24 +34,24 @@ public function initialize(Server $server) { public function propFind(PropFind $propFind, INode $node) { if ($node instanceof FacePhoto) { - $propFind->handle('{http://nextcloud.org/ns}file-name', function () use ($node) { + $propFind->handle(self::FILE_NAME_PROPERTYNAME, function () use ($node) { return $node->getFile()->getName(); }); - $propFind->handle('{http://nextcloud.org/ns}face-detections', function () use ($node) { + $propFind->handle(self::FACE_DETECTIONS_PROPERTYNAME, function () use ($node) { return json_encode(array_map(fn (FaceDetectionWithTitle $face) => $face->toArray(), array_values(array_filter($this->faceDetectionMapper->findByFileIdWithTitle($node->getFile()->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); }); - $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, fn () => $node->getFile()->getId()); - $propFind->handle('{http://nextcloud.org/ns}file-name', fn () => $node->getFile()->getName()); - $propFind->handle('{http://nextcloud.org/ns}realpath', fn () => $node->getFile()->getPath()); - $propFind->handle(self::FILE_METADATA_SIZE, fn () => json_encode($node->getMetadata())); - $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, fn () => json_encode($node->hasPreview())); - $propFind->handle(self::FAVORITE_PROPERTYNAME, fn () => $node->isFavorite() ? 1 : 0); + $propFind->handle(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, fn () => $node->getFile()->getId()); + $propFind->handle(self::FILE_NAME_PROPERTYNAME, fn () => $node->getFile()->getName()); + $propFind->handle(self::REALPATH_PROPERTYNAME, fn () => $node->getFile()->getPath()); + $propFind->handle(FilesPlugin::FILE_METADATA_SIZE, fn () => json_encode($node->getMetadata())); + $propFind->handle(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, fn () => json_encode($node->hasPreview())); + $propFind->handle(TagsPlugin::FAVORITE_PROPERTYNAME, fn () => $node->isFavorite() ? 1 : 0); } if ($node instanceof File) { - $propFind->handle('{http://nextcloud.org/ns}face-detections', function () use ($node) { + $propFind->handle(self::FACE_DETECTIONS_PROPERTYNAME, function () use ($node) { return json_encode(array_map(fn (FaceDetectionWithTitle $face) => $face->toArray(), array_values(array_filter($this->faceDetectionMapper->findByFileIdWithTitle($node->getId()), fn (FaceDetection $face) => $face->getClusterId() !== null)))); }); } From 2b2bf461c710a0c871045bfb7db4ae27d315236d Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 19 Aug 2022 10:43:34 +0200 Subject: [PATCH 08/14] FaceRoot: Cache children Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FaceRoot.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php index 8ed00d73f..4dcd2022b 100644 --- a/lib/Dav/Faces/FaceRoot.php +++ b/lib/Dav/Faces/FaceRoot.php @@ -26,6 +26,10 @@ class FaceRoot implements ICollection, IMoveTarget { private IMetadataManager $metadataManager; private ITagManager $tagManager; private IPreview $previewManager; + /** + * @var \OCA\Recognize\Dav\Faces\FacePhoto[] + */ + private array $children; public function __construct(FaceClusterMapper $clusterMapper, FaceCluster $cluster, IUser $user, FaceDetectionMapper $detectionMapper, IRootFolder $rootFolder, IMetadataManager $metadataManager, ITagManager $tagManager, IPreview $previewManager) { $this->clusterMapper = $clusterMapper; @@ -72,9 +76,12 @@ public function createFile($name, $data = null) { * @throws \OC\User\NoUserException */ public function getChildren(): array { - return array_map(function (FaceDetection $detection) { - return new FacePhoto($this->detectionMapper, $this->cluster, $detection, $this->rootFolder->getUserFolder($this->user->getUID()), $this->tagManager, $this->metadataManager, $this->previewManager); - }, $this->detectionMapper->findByClusterId($this->cluster->getId())); + if (count($this->children) === 0) { + $this->children = array_map(function (FaceDetection $detection) { + return new FacePhoto($this->detectionMapper, $this->cluster, $detection, $this->rootFolder->getUserFolder($this->user->getUID()), $this->tagManager, $this->metadataManager, $this->previewManager); + }, $this->detectionMapper->findByClusterId($this->cluster->getId())); + } + return $this->children; } public function getChild($name): FacePhoto { From 1aa642cfcc5a5d2ce320bed050d4c79553dceb50 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 19 Aug 2022 11:46:27 +0200 Subject: [PATCH 09/14] Update lib/Dav/Faces/FaceRoot.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julius Härtl Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FaceRoot.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php index 4dcd2022b..1c0ad17fd 100644 --- a/lib/Dav/Faces/FaceRoot.php +++ b/lib/Dav/Faces/FaceRoot.php @@ -29,7 +29,7 @@ class FaceRoot implements ICollection, IMoveTarget { /** * @var \OCA\Recognize\Dav\Faces\FacePhoto[] */ - private array $children; + private array $children = []; public function __construct(FaceClusterMapper $clusterMapper, FaceCluster $cluster, IUser $user, FaceDetectionMapper $detectionMapper, IRootFolder $rootFolder, IMetadataManager $metadataManager, ITagManager $tagManager, IPreview $previewManager) { $this->clusterMapper = $clusterMapper; From dfb3dbcfece538996cd1ad4a14835782ad478901 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Tue, 23 Aug 2022 12:27:14 +0200 Subject: [PATCH 10/14] PropFindPlugin: Fix import Signed-off-by: Marcel Klehr --- lib/Dav/Faces/PropFindPlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Dav/Faces/PropFindPlugin.php b/lib/Dav/Faces/PropFindPlugin.php index 7a95cb200..d54788e6c 100644 --- a/lib/Dav/Faces/PropFindPlugin.php +++ b/lib/Dav/Faces/PropFindPlugin.php @@ -3,8 +3,8 @@ namespace OCA\Recognize\Dav\Faces; use OCA\DAV\Connector\Sabre\File; -use OCA\DAV\Connector\Sabre\FilesPlugin; -use OCA\DAV\Connector\Sabre\TagsPlugin; +use \OCA\DAV\Connector\Sabre\FilesPlugin; +use \OCA\DAV\Connector\Sabre\TagsPlugin; use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; use OCA\Recognize\Db\FaceDetectionWithTitle; From 94a2ff3765a9f9fa5185e5a21bac3184c932f974 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Tue, 23 Aug 2022 12:27:38 +0200 Subject: [PATCH 11/14] FacePhoto#getMetadata: Fix return type Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FacePhoto.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Dav/Faces/FacePhoto.php b/lib/Dav/Faces/FacePhoto.php index 2c5eefd86..6ec46ee0e 100644 --- a/lib/Dav/Faces/FacePhoto.php +++ b/lib/Dav/Faces/FacePhoto.php @@ -128,7 +128,7 @@ public function getLastModified() { return $this->getFile()->getMTime(); } - public function getMetadata(): string { + public function getMetadata(): array { $file = $this->getFile(); $sizeMetadata = $this->metadataManager->fetchMetadataFor('size', [$file->getId()])[$file->getId()]; return $sizeMetadata->getMetadata(); From 61e239adb900418ebb2cee373c08b3f2fe294944 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Tue, 23 Aug 2022 12:28:10 +0200 Subject: [PATCH 12/14] Dav endpoints getChild: Do not query all children to get a single one Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FaceRoot.php | 12 +++++++----- lib/Dav/Faces/FacesHome.php | 15 +++++++++++---- lib/Db/FaceClusterMapper.php | 17 +++++++++++++++++ lib/Db/FaceDetectionMapper.php | 15 +++++++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php index 1c0ad17fd..c3f2c7b63 100644 --- a/lib/Dav/Faces/FaceRoot.php +++ b/lib/Dav/Faces/FaceRoot.php @@ -7,6 +7,7 @@ use OCA\Recognize\Db\FaceClusterMapper; use OCA\Recognize\Db\FaceDetection; use OCA\Recognize\Db\FaceDetectionMapper; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\Files\IRootFolder; use OCP\IPreview; use OCP\ITagManager; @@ -85,12 +86,13 @@ public function getChildren(): array { } public function getChild($name): FacePhoto { - foreach ($this->getChildren() as $child) { - if ($child->getName() === $name) { - return $child; - } + [$fileId,] = explode('-', $name); + try { + $detection = $this->detectionMapper->findByFileIdAndClusterId((int)$fileId, $this->cluster->getId()); + } catch (DoesNotExistException $e) { + throw new NotFound(); } - throw new NotFound("$name not found"); + return new FacePhoto($this->detectionMapper, $this->cluster, $detection, $this->rootFolder->getUserFolder($this->user->getUID()), $this->tagManager, $this->metadataManager, $this->previewManager); } public function childExists($name): bool { diff --git a/lib/Dav/Faces/FacesHome.php b/lib/Dav/Faces/FacesHome.php index 57c48b7bd..42dafecf2 100644 --- a/lib/Dav/Faces/FacesHome.php +++ b/lib/Dav/Faces/FacesHome.php @@ -6,6 +6,9 @@ use OCA\Recognize\Db\FaceCluster; use OCA\Recognize\Db\FaceClusterMapper; use OCA\Recognize\Db\FaceDetectionMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\DB\Exception; use OCP\Files\IRootFolder; use OCP\IPreview; use OCP\ITagManager; @@ -55,13 +58,17 @@ public function createFile($name, $data = null) { } public function getChild($name) { - foreach ($this->getChildren() as $child) { - if ($child->getName() === $name) { - return $child; + try { + $cluster = $this->faceClusterMapper->findByUserAndTitle($this->user->getUID(), $name); + } catch (DoesNotExistException $e) { + try { + $cluster = $this->faceClusterMapper->find((int) $name); + } catch (DoesNotExistException $e) { + throw new NotFound(); } } - throw new NotFound(); + return new FaceRoot($this->faceClusterMapper, $cluster, $this->user, $this->faceDetectionMapper, $this->rootFolder, $this->metadataManager, $this->tagManager, $this->previewManager); } /** diff --git a/lib/Db/FaceClusterMapper.php b/lib/Db/FaceClusterMapper.php index 954195897..d1844347f 100644 --- a/lib/Db/FaceClusterMapper.php +++ b/lib/Db/FaceClusterMapper.php @@ -13,6 +13,8 @@ public function __construct(IDBConnection $db) { } /** + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws \OCP\DB\Exception */ public function find(int $id): FaceCluster { @@ -34,6 +36,21 @@ public function findByUserId(string $userId): array { return $this->findEntities($qb); } + + /** + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws \OCP\DB\Exception + */ + public function findByUserAndTitle(string $userId, string $title) : Entity { + $qb = $this->db->getQueryBuilder(); + $qb->select(FaceCluster::$columns) + ->from('recognize_face_clusters') + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))) + ->andWhere($qb->expr()->eq('title', $qb->createPositionalParameter($title))); + return $this->findEntity($qb); + } + /** * @throws \OCP\DB\Exception */ diff --git a/lib/Db/FaceDetectionMapper.php b/lib/Db/FaceDetectionMapper.php index 4c1bdac9b..365991da9 100644 --- a/lib/Db/FaceDetectionMapper.php +++ b/lib/Db/FaceDetectionMapper.php @@ -78,6 +78,21 @@ public function findByFileIdWithTitle(int $fileId) { return $this->findEntities($qb); } + /** + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws \OCP\DB\Exception + */ + public function findByFileIdAndClusterId(int $fileId, int $clusterId) : FaceDetection { + $qb = $this->db->getQueryBuilder(); + $qb->select(FaceDetection::$columns) + ->from('recognize_face_detections', 'd') + ->leftJoin('d', 'recognize_face_clusters', 'c', $qb->expr()->eq('d.cluster_id', 'c.id')) + ->where($qb->expr()->eq('d.file_id', $qb->createPositionalParameter($fileId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('c.id', $qb->createPositionalParameter($clusterId, IQueryBuilder::PARAM_INT))); + return $this->findEntity($qb); + } + protected function mapRowToEntity(array $row): Entity { try { return parent::mapRowToEntity($row); From 1d4f9af4c98a085d2407f774890eeffe6a8468de Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Tue, 23 Aug 2022 12:41:57 +0200 Subject: [PATCH 13/14] FacesHome dav endpoint: Avoid returning clusters that don't belong to this user Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FacesHome.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Dav/Faces/FacesHome.php b/lib/Dav/Faces/FacesHome.php index 42dafecf2..ddd126269 100644 --- a/lib/Dav/Faces/FacesHome.php +++ b/lib/Dav/Faces/FacesHome.php @@ -63,6 +63,9 @@ public function getChild($name) { } catch (DoesNotExistException $e) { try { $cluster = $this->faceClusterMapper->find((int) $name); + if ($cluster->getUserId() !== $this->user->getUID()) { + throw new NotFound(); + } } catch (DoesNotExistException $e) { throw new NotFound(); } From 0b1f0d02b62f086f7e913b3a1437a3ea59f59804 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 24 Aug 2022 12:14:01 +0200 Subject: [PATCH 14/14] getChild: Use children cache if possible Signed-off-by: Marcel Klehr --- lib/Dav/Faces/FaceRoot.php | 8 ++++++++ lib/Dav/Faces/FacesHome.php | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/Dav/Faces/FaceRoot.php b/lib/Dav/Faces/FaceRoot.php index c3f2c7b63..b47204750 100644 --- a/lib/Dav/Faces/FaceRoot.php +++ b/lib/Dav/Faces/FaceRoot.php @@ -86,6 +86,14 @@ public function getChildren(): array { } public function getChild($name): FacePhoto { + if (count($this->children) !== 0) { + foreach ($this->getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new NotFound("$name not found"); + } [$fileId,] = explode('-', $name); try { $detection = $this->detectionMapper->findByFileIdAndClusterId((int)$fileId, $this->cluster->getId()); diff --git a/lib/Dav/Faces/FacesHome.php b/lib/Dav/Faces/FacesHome.php index ddd126269..f40f81b7c 100644 --- a/lib/Dav/Faces/FacesHome.php +++ b/lib/Dav/Faces/FacesHome.php @@ -57,7 +57,15 @@ public function createFile($name, $data = null) { throw new Forbidden('Not allowed to create files in this folder'); } - public function getChild($name) { + public function getChild($name) : FaceRoot { + if (count($this->children) !== 0) { + foreach ($this->getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new NotFound(); + } try { $cluster = $this->faceClusterMapper->findByUserAndTitle($this->user->getUID(), $name); } catch (DoesNotExistException $e) {