Skip to content

Commit

Permalink
PoC: SystemTags endpoint to return tags used by a user with meta data
Browse files Browse the repository at this point in the history
Target case is photos app: when visiting the tags category, all systemtags
of the whole cloud are retrieved. In subequent steps the next tag is
requested until the browser view is filled with tag tiles (i.e. previews
are requested just as well).

With this approach, we incorpoate the dav search and look for user related
tags that are used by them, and already returns the statistics (number of
files tagged with the respective tag) as well as a file id for the purpose
to load the preview. This defaults to the file with the highest id.

Call:
curl -s -u 'user:password' \
  'https://my.nc.srv/remote.php/dav/systemtags-current' \
  -X PROPFIND -H 'Accept: text/plain' \
  -H 'Accept-Language: en-US,en;q=0.5'  -H 'Depth: 1' \
  -H 'Content-Type: text/plain;charset=UTF-8' \
  --data @/home/doe/request-systemtag-props.xml

With request-systemtag-props.xml:
<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
        <d:prop xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
                <oc:id/>
                <oc:display-name/>
                <oc:user-visible/>
                <oc:user-assignable/>
                <oc:can-assign/>
                <nc:files-assigned/>
                <nc:reference-fileid/>
        </d:prop>
</d:propfind>

Example output:
  …
  <d:response>
    <d:href>/master/remote.php/dav/systemtags/84</d:href>
    <d:propstat>
      <d:prop>
        <oc:id>84</oc:id>
        <oc:display-name>Computer</oc:display-name>
        <oc:user-visible>true</oc:user-visible>
        <oc:user-assignable>true</oc:user-assignable>
        <oc:can-assign>true</oc:can-assign>
        <nc:files-assigned>42</nc:files-assigned>
        <nc:reference-fileid>924022</nc:reference-fileid>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
  </d:response>
  <d:response>
    <d:href>/remote.php/dav/systemtags/97</d:href>
    <d:propstat>
      <d:prop>
        <oc:id>97</oc:id>
        <oc:display-name>Bear</oc:display-name>
        <oc:user-visible>true</oc:user-visible>
        <oc:user-assignable>true</oc:user-assignable>
        <oc:can-assign>true</oc:can-assign>
        <nc:files-assigned>1</nc:files-assigned>
        <nc:reference-fileid>923422</nc:reference-fileid>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
  </d:response>
  …

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
  • Loading branch information
blizzz committed Apr 28, 2023
1 parent 32219ec commit c174172
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 46 deletions.
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
'OCA\\DAV\\SystemTag\\SystemTagNode' => $baseDir . '/../lib/SystemTag/SystemTagNode.php',
'OCA\\DAV\\SystemTag\\SystemTagPlugin' => $baseDir . '/../lib/SystemTag/SystemTagPlugin.php',
'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => $baseDir . '/../lib/SystemTag/SystemTagsByIdCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => $baseDir . '/../lib/SystemTag/SystemTagsInUseCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => $baseDir . '/../lib/SystemTag/SystemTagsRelationsCollection.php',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\SystemTag\\SystemTagNode' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagNode.php',
'OCA\\DAV\\SystemTag\\SystemTagPlugin' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagPlugin.php',
'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsByIdCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsInUseCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsRelationsCollection.php',
Expand Down
7 changes: 7 additions & 0 deletions apps/dav/lib/RootCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Sabre\DAV\SimpleCollection;
Expand All @@ -65,6 +66,7 @@ public function __construct() {
$dispatcher = \OC::$server->get(IEventDispatcher::class);
$config = \OC::$server->get(IConfig::class);
$proxyMapper = \OC::$server->query(ProxyMapper::class);
$rootFolder = \OCP\Server::get(IRootFolder::class);

$userPrincipalBackend = new Principal(
$userManager,
Expand Down Expand Up @@ -131,6 +133,10 @@ public function __construct() {
$groupManager,
\OC::$server->getEventDispatcher()
);
$systemTagInUseCollection = new SystemTag\SystemTagsInUseCollection(
$userSession,
$rootFolder
);
$commentsCollection = new Comments\RootCollection(
\OC::$server->getCommentsManager(),
$userManager,
Expand Down Expand Up @@ -179,6 +185,7 @@ public function __construct() {
$systemAddressBookRoot]),
$systemTagCollection,
$systemTagRelationsCollection,
$systemTagInUseCollection,
$commentsCollection,
$uploadCollection,
$avatarCollection,
Expand Down
19 changes: 19 additions & 0 deletions apps/dav/lib/SystemTag/SystemTagNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ class SystemTagNode implements \Sabre\DAV\INode {
*/
protected $isAdmin;

protected int $numberOfFiles = -1;
protected int $referenceFileId = -1;

/**
* Sets up the node, expects a full path name
*
Expand Down Expand Up @@ -172,4 +175,20 @@ public function delete() {
throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found', 0, $e);
}
}

public function getNumberOfFiles(): int {
return $this->numberOfFiles;
}

public function setNumberOfFiles(int $numberOfFiles): void {
$this->numberOfFiles = $numberOfFiles;
}

public function getReferenceFileId(): int {
return $this->referenceFileId;
}

public function setReferenceFileId(int $referenceFileId): void {
$this->referenceFileId = $referenceFileId;
}
}
24 changes: 24 additions & 0 deletions apps/dav/lib/SystemTag/SystemTagPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
public const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable';
public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups';
public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign';
public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned';
public const FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';

/**
* @var \Sabre\DAV\Server $server
Expand Down Expand Up @@ -224,6 +226,11 @@ public function handleGetProperties(
return;
}

// child nodes from systemtags-current should point to normal tag endpoint
if (preg_match('/^systemtags-current\/[0-9]+/', $propFind->getPath())) {
$propFind->setPath(str_replace('systemtags-current/', 'systemtags/', $propFind->getPath()));
}

$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
return $node->getSystemTag()->getId();
});
Expand Down Expand Up @@ -258,6 +265,16 @@ public function handleGetProperties(
}
return implode('|', $groups);
});

if ($node instanceof SystemTagNode) {
$propFind->handle(self::NUM_FILES_PROPERTYNAME, function () use ($node): int {
return $node->getNumberOfFiles();
});

$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node): int {
return $node->getReferenceFileId();
});
}
}

/**
Expand All @@ -279,6 +296,8 @@ public function handleUpdateProperties($path, PropPatch $propPatch) {
self::USERVISIBLE_PROPERTYNAME,
self::USERASSIGNABLE_PROPERTYNAME,
self::GROUPS_PROPERTYNAME,
self::NUM_FILES_PROPERTYNAME,
self::FILEID_PROPERTYNAME,
], function ($props) use ($node) {
$tag = $node->getSystemTag();
$name = $tag->getName();
Expand Down Expand Up @@ -315,6 +334,11 @@ public function handleUpdateProperties($path, PropPatch $propPatch) {
$this->tagManager->setTagGroups($tag, $groupIds);
}

if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::FILEID_PROPERTYNAME])) {
// read-only properties
throw new Forbidden();
}

if ($updateTag) {
$node->update($name, $userVisible, $userAssignable);
}
Expand Down
67 changes: 67 additions & 0 deletions apps/dav/lib/SystemTag/SystemTagsInUseCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

namespace OCA\DAV\SystemTag;

use OC\SystemTag\SystemTag;
use OCP\Files\IRootFolder;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
use Sabre\DAV\Exception\Forbidden;

class SystemTagsInUseCollection extends \Sabre\DAV\SimpleCollection {
protected IUserSession $userSession;
protected IRootFolder $rootFolder;

public function __construct(IUserSession $userSession, IRootFolder $rootFolder) {
$this->userSession = $userSession;
$this->rootFolder = $rootFolder;
$this->name = 'systemtags-current';
}

public function setName($name): void {
throw new Forbidden('Permission denied to rename this collection');
}

public function getChildren() {
$user = $this->userSession->getUser();
if ($user === null) {
throw new Forbidden('Permission denied to read this collection');
}

$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$result = $userFolder->getSystemTags('image');
$children = [];
foreach ($result as $tagData) {
$tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable']);
$node = new SystemTagNode($tag, $user, false, \OCP\Server::get(ISystemTagManager::class));
$node->setNumberOfFiles($tagData['number_files']);
$node->setReferenceFileId($tagData['ref_file_id']);
$children[] = $node;
}
return $children;
}
}
22 changes: 21 additions & 1 deletion lib/private/Files/Cache/CacheQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,28 @@ public function __construct(IDBConnection $connection, SystemConfig $systemConfi
parent::__construct($connection, $systemConfig, $logger);
}

public function selectTagUsage(): self {
$this
->select('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable')
->selectAlias($this->createFunction('COUNT(filecache.fileid)'), 'number_files')
->selectAlias($this->createFunction('MAX(filecache.fileid)'), 'ref_file_id')
->from('filecache', 'filecache')
->leftJoin('filecache', 'systemtag_object_mapping', 'systemtagmap', $this->expr()->andX(
$this->expr()->eq('filecache.fileid', $this->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)),
$this->expr()->eq('systemtagmap.objecttype', $this->createNamedParameter('files'))
))
->leftJoin('systemtagmap', 'systemtag', 'systemtag', $this->expr()->andX(
$this->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'),
$this->expr()->eq('systemtag.visibility', $this->createNamedParameter(true))
))
->where($this->expr()->like('systemtag.name', $this->createNamedParameter('_%')))
->groupBy('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable');

return $this;
}

public function selectFileCache(string $alias = null, bool $joinExtendedCache = true) {
$name = $alias ? $alias : 'filecache';
$name = $alias ?: 'filecache';
$this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", "$name.name", 'mimetype', 'mimepart', 'size', 'mtime',
'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum', 'unencrypted_size')
->from('filecache', $name);
Expand Down
58 changes: 37 additions & 21 deletions lib/private/Files/Cache/QuerySearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,41 @@ protected function getQueryBuilder() {
);
}

protected function applySearchConstraints(CacheQueryBuilder $query, ISearchQuery $searchQuery, array $caches): void {
$storageFilters = array_values(array_map(function (ICache $cache) {
return $cache->getQueryFilterForStorage();
}, $caches));
$storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters);
$filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]);
$this->queryOptimizer->processOperator($filter);

$searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter);
if ($searchExpr) {
$query->andWhere($searchExpr);
}

$this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder());

if ($searchQuery->getLimit()) {
$query->setMaxResults($searchQuery->getLimit());
}
if ($searchQuery->getOffset()) {
$query->setFirstResult($searchQuery->getOffset());
}
}

public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): array {
$query = $this->getQueryBuilder();
$query->selectTagUsage();

$this->applySearchConstraints($query, $searchQuery, $caches);

$result = $query->execute();
$tags = $result->fetchAll();
$result->closeCursor();
return $tags;
}

/**
* Perform a file system search in multiple caches
*
Expand Down Expand Up @@ -127,26 +162,7 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array
));
}

$storageFilters = array_values(array_map(function (ICache $cache) {
return $cache->getQueryFilterForStorage();
}, $caches));
$storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters);
$filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]);
$this->queryOptimizer->processOperator($filter);

$searchExpr = $this->searchBuilder->searchOperatorToDBExpr($builder, $filter);
if ($searchExpr) {
$query->andWhere($searchExpr);
}

$this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder());

if ($searchQuery->getLimit()) {
$query->setMaxResults($searchQuery->getLimit());
}
if ($searchQuery->getOffset()) {
$query->setFirstResult($searchQuery->getOffset());
}
$this->applySearchConstraints($query, $searchQuery, $caches);

$result = $query->execute();
$files = $result->fetchAll();
Expand All @@ -158,7 +174,7 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array
$result->closeCursor();

// loop through all caches for each result to see if the result matches that storage
// results are grouped by the same array keys as the caches argument to allow the caller to distringuish the source of the results
// results are grouped by the same array keys as the caches argument to allow the caller to distinguish the source of the results
$results = array_fill_keys(array_keys($caches), []);
foreach ($rawEntries as $rawEntry) {
foreach ($caches as $cacheKey => $cache) {
Expand Down
Loading

0 comments on commit c174172

Please sign in to comment.