Skip to content

Commit 771d97c

Browse files
feat(recent-files): allow grouping search results by mime type
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
1 parent ec03e87 commit 771d97c

7 files changed

Lines changed: 162 additions & 3 deletions

File tree

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@
239239
'OCA\\DAV\\Connector\\Sabre\\File' => $baseDir . '/../lib/Connector/Sabre/File.php',
240240
'OCA\\DAV\\Connector\\Sabre\\FilesPlugin' => $baseDir . '/../lib/Connector/Sabre/FilesPlugin.php',
241241
'OCA\\DAV\\Connector\\Sabre\\FilesReportPlugin' => $baseDir . '/../lib/Connector/Sabre/FilesReportPlugin.php',
242+
'OCA\\DAV\\Connector\\Sabre\\GroupableFile' => $baseDir . '/../lib/Connector/Sabre/GroupableFile.php',
242243
'OCA\\DAV\\Connector\\Sabre\\LockPlugin' => $baseDir . '/../lib/Connector/Sabre/LockPlugin.php',
243244
'OCA\\DAV\\Connector\\Sabre\\MaintenancePlugin' => $baseDir . '/../lib/Connector/Sabre/MaintenancePlugin.php',
244245
'OCA\\DAV\\Connector\\Sabre\\MtimeSanitizer' => $baseDir . '/../lib/Connector/Sabre/MtimeSanitizer.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ class ComposerStaticInitDAV
254254
'OCA\\DAV\\Connector\\Sabre\\File' => __DIR__ . '/..' . '/../lib/Connector/Sabre/File.php',
255255
'OCA\\DAV\\Connector\\Sabre\\FilesPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/FilesPlugin.php',
256256
'OCA\\DAV\\Connector\\Sabre\\FilesReportPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/FilesReportPlugin.php',
257+
'OCA\\DAV\\Connector\\Sabre\\GroupableFile' => __DIR__ . '/..' . '/../lib/Connector/Sabre/GroupableFile.php',
257258
'OCA\\DAV\\Connector\\Sabre\\LockPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/LockPlugin.php',
258259
'OCA\\DAV\\Connector\\Sabre\\MaintenancePlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/MaintenancePlugin.php',
259260
'OCA\\DAV\\Connector\\Sabre\\MtimeSanitizer' => __DIR__ . '/..' . '/../lib/Connector/Sabre/MtimeSanitizer.php',

apps/dav/lib/Connector/Sabre/FilesPlugin.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class FilesPlugin extends ServerPlugin {
7070
public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
7171
public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
7272
public const LAST_ACTIVITY_PROPERTYNAME = '{http://nextcloud.org/ns}last_activity';
73+
public const MIME_TYPE_GROUP = '{http://nextcloud.org/ns}mime_type_group';
7374
public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
7475
public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
7576
public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
@@ -453,6 +454,10 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node)
453454
return $node->getFileInfo()->getLastActivity();
454455
});
455456

457+
$propFind->handle(self::MIME_TYPE_GROUP, function () use ($node) {
458+
return $node instanceof GroupableFile ? $node->getGroup() : null;
459+
});
460+
456461
foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
457462
$propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, fn () => $metadataValue);
458463
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-only
6+
*/
7+
8+
namespace OCA\DAV\Connector\Sabre;
9+
10+
use OC\Files\View;
11+
use OCA\DAV\Connector\Sabre\File;
12+
use OCP\Files\FileInfo;
13+
use OCP\IL10N;
14+
use OCP\IRequest;
15+
use OCP\Share\IManager;
16+
17+
class GroupableFile extends File {
18+
19+
public function __construct(
20+
View $view,
21+
FileInfo $info,
22+
?IManager $shareManager = null,
23+
?IRequest $request = null,
24+
?IL10N $l10n = null,
25+
protected ?int $group = null,
26+
) {
27+
parent::__construct($view, $info, $shareManager, $request, $l10n);
28+
}
29+
30+
public function getGroup(): ?int {
31+
return $this->group;
32+
}
33+
34+
public function setGroup(int $group): void {
35+
$this->group = $group;
36+
}
37+
}

apps/dav/lib/Files/FileSearchBackend.php

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
use OC\Files\Search\SearchQuery;
1414
use OC\Files\Storage\Wrapper\Jail;
1515
use OC\Files\View;
16+
use OCA\Files\AppInfo\Application;
17+
use OCA\Files\ConfigLexicon;
1618
use OCA\DAV\Connector\Sabre\CachingTree;
1719
use OCA\DAV\Connector\Sabre\Directory;
1820
use OCA\DAV\Connector\Sabre\File;
1921
use OCA\DAV\Connector\Sabre\FilesPlugin;
22+
use OCA\DAV\Connector\Sabre\GroupableFile;
2023
use OCA\DAV\Connector\Sabre\Server;
2124
use OCA\DAV\Connector\Sabre\TagsPlugin;
2225
use OCP\Files\Cache\ICacheEntry;
@@ -31,6 +34,7 @@
3134
use OCP\FilesMetadata\IFilesMetadataManager;
3235
use OCP\FilesMetadata\IMetadataQuery;
3336
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
37+
use OCP\IAppConfig;
3438
use OCP\IUser;
3539
use OCP\Share\IManager;
3640
use Sabre\DAV\Exception\NotFound;
@@ -54,6 +58,7 @@ public function __construct(
5458
private IManager $shareManager,
5559
private View $view,
5660
private IFilesMetadataManager $filesMetadataManager,
61+
private IAppConfig $appConfig,
5762
) {
5863
}
5964

@@ -216,10 +221,14 @@ public function search(Query $search): array {
216221
$results = $userFolder->search($query);
217222
}
218223

224+
$groupRecentFilesEnabled = $this->appConfig->getValueBool(Application::APP_ID, ConfigLexicon::GROUP_RECENT_FILES, false);
225+
219226
/** @var SearchResult[] $nodes */
220-
$nodes = array_map(function (Node $node) {
227+
$nodes = array_map(function (Node $node) use ($groupRecentFilesEnabled) {
221228
if ($node instanceof Folder) {
222229
$davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
230+
} elseif ($groupRecentFilesEnabled) {
231+
$davNode = new GroupableFile($this->view, $node, $this->shareManager);
223232
} else {
224233
$davNode = new File($this->view, $node, $this->shareManager);
225234
}
@@ -228,6 +237,10 @@ public function search(Query $search): array {
228237
return new SearchResult($davNode, $path);
229238
}, $results);
230239

240+
if ($groupRecentFilesEnabled) {
241+
$nodes = $this->setGroupOnNodes($nodes);
242+
}
243+
231244
if (!$query->limitToHome()) {
232245
// Sort again, since the result from multiple storages is appended and not sorted
233246
usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
@@ -572,4 +585,103 @@ private function extractWhereValue(Operator &$operator, string $propertyName, st
572585
return null;
573586
}
574587
}
588+
589+
/**
590+
* @param SearchResult[] $searchResults
591+
* @return SearchResult[] $searchResults
592+
*/
593+
private function setGroupOnNodes(array $searchResults): array {
594+
$mimeTypes = $this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::RECENT_FILES_GROUP_MIME_TYPES, []);
595+
if (count($mimeTypes) === 0) {
596+
return $searchResults;
597+
}
598+
$timespanMinutes = $this->appConfig->getValueInt(Application::APP_ID, ConfigLexicon::RECENT_FILES_GROUP_TIMESPAN_MINUTES, 2);
599+
$timespan = $timespanMinutes * 60;
600+
601+
// sort by oldest action to the most recent
602+
usort($searchResults, fn($a, $b) => $this->getNodeTime($a) <=> $this->getNodeTime($b));
603+
604+
$count = count($searchResults);
605+
$result = [];
606+
$groupNumber = 1;
607+
$i = 0;
608+
609+
while ($i < $count) {
610+
$current = $searchResults[$i];
611+
612+
if (!$this->isNodeGroupable($current, $mimeTypes)) {
613+
$result[] = $current;
614+
$i++;
615+
continue;
616+
}
617+
618+
$groupStartTime = $this->getNodeTime($current);
619+
$isContaminated = false;
620+
621+
// look ahead to check if the time window is contaminated by a non-groupable node
622+
for ($j = $i + 1; $j < $count; $j++) {
623+
$nextTime = $this->getNodeTime($searchResults[$j]);
624+
if (abs($nextTime - $groupStartTime) > $timespan) {
625+
break;
626+
}
627+
if (!$this->isNodeGroupable($searchResults[$j], $mimeTypes)) {
628+
$isContaminated = true;
629+
break;
630+
}
631+
}
632+
633+
if ($isContaminated) {
634+
$result[] = $current;
635+
$i++;
636+
continue;
637+
}
638+
639+
$groupIndexes = [$i];
640+
$i++;
641+
642+
// add nodes to group until time window limit is reached
643+
while ($i < $count) {
644+
$next = $searchResults[$i];
645+
$nextTime = $this->getNodeTime($next);
646+
647+
if (abs($nextTime - $groupStartTime) > $timespan) {
648+
break;
649+
}
650+
651+
$groupIndexes[] = $i;
652+
$i++;
653+
}
654+
655+
if (count($groupIndexes) === 1) {
656+
$result[] = $searchResults[$groupIndexes[0]];
657+
continue;
658+
}
659+
660+
foreach ($groupIndexes as $idx) {
661+
/** @var GroupableFile $node */
662+
$node = $searchResults[$idx]->node;
663+
$node->setGroup($groupNumber);
664+
$result[] = $searchResults[$idx];
665+
}
666+
$groupNumber++;
667+
}
668+
669+
return $result;
670+
}
671+
672+
private function getNodeTime(SearchResult $result): int {
673+
$node = $result->node;
674+
if (!$node instanceof GroupableFile) {
675+
return 0;
676+
}
677+
$uploadTime = $node->getNode()->getUploadTime();
678+
$creationTime = $node->getNode()->getCreationTime();
679+
$lastModified = $node->getLastModified();
680+
return max($uploadTime, $creationTime, $lastModified);
681+
}
682+
683+
private function isNodeGroupable(SearchResult $result, array $mimeTypes): bool {
684+
$node = $result->node;
685+
return $node instanceof GroupableFile && in_array($node->getNode()->getMimetype(), $mimeTypes, true);
686+
}
575687
}

apps/dav/lib/Server.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ public function __construct(
379379
\OCP\Server::get(IRootFolder::class),
380380
$shareManager,
381381
$view,
382-
\OCP\Server::get(IFilesMetadataManager::class)
382+
\OCP\Server::get(IFilesMetadataManager::class),
383+
\OCP\Server::get(IAppConfig::class),
383384
));
384385
$this->server->addPlugin(
385386
new BulkUploadPlugin(

apps/dav/tests/unit/Files/FileSearchBackendTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use OCA\DAV\Connector\Sabre\ObjectTree;
1818
use OCA\DAV\Connector\Sabre\Server;
1919
use OCA\DAV\Files\FileSearchBackend;
20+
use OCP\IAppConfig;
2021
use OCP\Files\FileInfo;
2122
use OCP\Files\Folder;
2223
use OCP\Files\IRootFolder;
@@ -80,8 +81,9 @@ protected function setUp(): void {
8081
->willReturn($this->searchFolder);
8182

8283
$filesMetadataManager = $this->createMock(IFilesMetadataManager::class);
84+
$appConfig = $this->createMock(IAppConfig::class);
8385

84-
$this->search = new FileSearchBackend($this->server, $this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view, $filesMetadataManager);
86+
$this->search = new FileSearchBackend($this->server, $this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view, $filesMetadataManager, $appConfig);
8587
}
8688

8789
public function testSearchFilename(): void {

0 commit comments

Comments
 (0)