From 9776782f4ad11f2d93f6adc19347b68237c85485 Mon Sep 17 00:00:00 2001 From: Cristian Scheid Date: Wed, 18 Mar 2026 09:29:52 -0300 Subject: [PATCH] feat(recent-files): add nc:last_activity property to allow sorting by max between upload_time and mtime Signed-off-by: Cristian Scheid Signed-off-by: nextcloud-command --- apps/dav/lib/Capabilities.php | 3 ++- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 6 ++++++ apps/dav/lib/Files/FileSearchBackend.php | 5 +++++ apps/dav/openapi.json | 6 +++++- apps/dav/tests/unit/CapabilitiesTest.php | 3 +++ apps/files/src/services/Recent.ts | 2 +- lib/private/Files/Cache/SearchBuilder.php | 4 ++++ lib/private/Files/Search/SearchOrder.php | 4 ++++ openapi.json | 6 +++++- 9 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php index 988c704f1fa..d88df0920fc 100644 --- a/apps/dav/lib/Capabilities.php +++ b/apps/dav/lib/Capabilities.php @@ -18,7 +18,7 @@ class Capabilities implements ICapability { } /** - * @return array{dav: array{chunking: string, public_shares_chunking: bool, search_supports_creation_time: bool, search_supports_upload_time: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}} + * @return array{dav: array{chunking: string, public_shares_chunking: bool, search_supports_creation_time: bool, search_supports_upload_time: bool, search_supports_last_activity: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}} */ public function getCapabilities() { $capabilities = [ @@ -27,6 +27,7 @@ class Capabilities implements ICapability { 'public_shares_chunking' => true, 'search_supports_creation_time' => true, 'search_supports_upload_time' => true, + 'search_supports_last_activity' => true, ] ]; if ($this->config->getSystemValueBool('bulkupload.enabled', true)) { diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 7b2f144dfa1..17ff62df6c3 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -67,6 +67,7 @@ class FilesPlugin extends ServerPlugin { public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag'; public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time'; public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time'; + public const LAST_ACTIVITY_PROPERTYNAME = '{http://nextcloud.org/ns}last_activity'; public const SHARE_NOTE = '{http://nextcloud.org/ns}note'; public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download'; public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count'; @@ -446,6 +447,11 @@ class FilesPlugin extends ServerPlugin { return $node->getFileInfo()->getCreationTime(); }); + $propFind->handle(self::LAST_ACTIVITY_PROPERTYNAME, function () use ($node) { + $fileInfo = $node->getFileInfo(); + return max($fileInfo->getUploadTime(), $fileInfo->getMTime()); + }); + foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) { $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue); } diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index 45888f1eac1..c14047eef50 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -88,6 +88,7 @@ class FileSearchBackend implements ISearchBackend { new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), new SearchPropertyDefinition('{DAV:}creationdate', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), new SearchPropertyDefinition('{http://nextcloud.org/ns}upload_time', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), + new SearchPropertyDefinition('{http://nextcloud.org/ns}last_activity', true, false, true, SearchPropertyDefinition::DATATYPE_DATETIME), new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN), new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), @@ -304,6 +305,8 @@ class FileSearchBackend implements ISearchBackend { return $node->getNode()->getCreationTime(); case '{http://nextcloud.org/ns}upload_time': return $node->getNode()->getUploadTime(); + case '{http://nextcloud.org/ns}last_activity': + return max($node->getNode()->getUploadTime(), $node->getNode()->getMTime()); case FilesPlugin::SIZE_PROPERTYNAME: return $node->getSize(); case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME: @@ -332,6 +335,8 @@ class FileSearchBackend implements ISearchBackend { $direction = $order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING; if (str_starts_with($order->property->name, FilesPlugin::FILE_METADATA_PREFIX)) { return new SearchOrder($direction, substr($order->property->name, strlen(FilesPlugin::FILE_METADATA_PREFIX)), IMetadataQuery::EXTRA); + } elseif ($order->property->name === FilesPlugin::LAST_ACTIVITY_PROPERTYNAME) { + return new SearchOrder($direction, 'last_activity'); } else { return new SearchOrder($direction, $this->mapPropertyNameToColumn($order->property)); } diff --git a/apps/dav/openapi.json b/apps/dav/openapi.json index c6ad08730c5..344d3781531 100644 --- a/apps/dav/openapi.json +++ b/apps/dav/openapi.json @@ -32,7 +32,8 @@ "chunking", "public_shares_chunking", "search_supports_creation_time", - "search_supports_upload_time" + "search_supports_upload_time", + "search_supports_last_activity" ], "properties": { "chunking": { @@ -47,6 +48,9 @@ "search_supports_upload_time": { "type": "boolean" }, + "search_supports_last_activity": { + "type": "boolean" + }, "bulkupload": { "type": "string" }, diff --git a/apps/dav/tests/unit/CapabilitiesTest.php b/apps/dav/tests/unit/CapabilitiesTest.php index 24297936a64..cfbcb2155fd 100644 --- a/apps/dav/tests/unit/CapabilitiesTest.php +++ b/apps/dav/tests/unit/CapabilitiesTest.php @@ -33,6 +33,7 @@ class CapabilitiesTest extends TestCase { 'public_shares_chunking' => true, 'search_supports_creation_time' => true, 'search_supports_upload_time' => true, + 'search_supports_last_activity' => true, ], ]; $this->assertSame($expected, $capabilities->getCapabilities()); @@ -55,6 +56,7 @@ class CapabilitiesTest extends TestCase { 'public_shares_chunking' => true, 'search_supports_creation_time' => true, 'search_supports_upload_time' => true, + 'search_supports_last_activity' => true, 'bulkupload' => '1.0', ], ]; @@ -78,6 +80,7 @@ class CapabilitiesTest extends TestCase { 'public_shares_chunking' => true, 'search_supports_creation_time' => true, 'search_supports_upload_time' => true, + 'search_supports_last_activity' => true, 'absence-supported' => true, 'absence-replacement' => true, ], diff --git a/apps/files/src/services/Recent.ts b/apps/files/src/services/Recent.ts index 484bf7cf230..403f4749a43 100644 --- a/apps/files/src/services/Recent.ts +++ b/apps/files/src/services/Recent.ts @@ -41,7 +41,7 @@ export async function getContents(path = '/', options: { signal: AbortSignal }): const contentsResponse = await client.search('/', { signal: options.signal, details: true, - data: getRecentSearch(lastTwoWeeksTimestamp, store.userConfig.recent_files_limit), + data: getRecentSearch(lastTwoWeeksTimestamp, store.userConfig.recent_files_limit + 1), }) as ResponseDataDetailed const contents = contentsResponse.data.results diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index 4f012c8f208..53a6c6f6596 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -352,6 +352,10 @@ class SearchBuilder { if ($field === 'mtime') { $field = $query->func()->add($field, $query->createNamedParameter(0)); } + + if ($field === 'last_activity') { + $field = $query->func()->greatest('file.mtime', $query->createFunction('COALESCE(fe.upload_time, 0)')); + } } $query->addOrderBy($field, $order->getDirection()); } diff --git a/lib/private/Files/Search/SearchOrder.php b/lib/private/Files/Search/SearchOrder.php index 5a036653f4e..3d3b9cb8464 100644 --- a/lib/private/Files/Search/SearchOrder.php +++ b/lib/private/Files/Search/SearchOrder.php @@ -58,6 +58,10 @@ class SearchOrder implements ISearchOrder { return $a->getId() <=> $b->getId(); case 'permissions': return $a->getPermissions() <=> $b->getPermissions(); + case 'last_activity': + $timeA = max($a->getUploadTime(), $a->getMtime()); + $timeB = max($b->getUploadTime(), $b->getMtime()); + return $timeA <=> $timeB; default: return 0; } diff --git a/openapi.json b/openapi.json index 8512113b614..745786c91bc 100644 --- a/openapi.json +++ b/openapi.json @@ -1506,7 +1506,8 @@ "chunking", "public_shares_chunking", "search_supports_creation_time", - "search_supports_upload_time" + "search_supports_upload_time", + "search_supports_last_activity" ], "properties": { "chunking": { @@ -1521,6 +1522,9 @@ "search_supports_upload_time": { "type": "boolean" }, + "search_supports_last_activity": { + "type": "boolean" + }, "bulkupload": { "type": "string" },