From 2b362a0f6bcd361c1da7ac96835ff1c841c59608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= <14975046+skjnldsv@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:06:26 +0100 Subject: [PATCH 1/3] fix(files): include recently uploaded files in Folder::getRecent() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uploads keep the mtime of the file they come from, so a file uploaded today with an older mtime was not recent for getRecent(), while the Files app's recent view, which looks at the upload time as well, shows it. Use the same last activity, the later of both, here. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com> --- lib/private/Files/Node/Folder.php | 17 +++++---- tests/lib/Files/Node/FolderTest.php | 53 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 7a58c41f60d1f..764eafae6e327 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -501,10 +501,15 @@ public function getRecent($limit, $offset = 0) { ] ); - $filterNonRecentFiles = new SearchComparison( - ISearchComparison::COMPARE_GREATER_THAN, - 'mtime', - strtotime('-2 week') + // Uploads keep the mtime of the file they come from, so look at when + // they were uploaded too, like the Files app's recent view does + $since = strtotime('-2 week'); + $filterNonRecentFiles = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', $since), + new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'upload_time', $since), + ] ); if ($offset === 0 && $limit <= 100) { $query = new SearchQuery( @@ -520,7 +525,7 @@ public function getRecent($limit, $offset = 0) { [ new SearchOrder( ISearchOrder::DIRECTION_DESCENDING, - 'mtime' + 'last_activity' ), ] ); @@ -532,7 +537,7 @@ public function getRecent($limit, $offset = 0) { [ new SearchOrder( ISearchOrder::DIRECTION_DESCENDING, - 'mtime' + 'last_activity' ), ] ); diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php index 55632711b1883..5a6881b31430b 100644 --- a/tests/lib/Files/Node/FolderTest.php +++ b/tests/lib/Files/Node/FolderTest.php @@ -820,6 +820,59 @@ public function testRecent(): void { $this->assertEquals([$id1, $id2, $id3], $ids); } + public function testRecentIncludesUploads(): void { + $manager = $this->createMock(Manager::class); + $view = $this->getRootViewMock(); + /** @var \PHPUnit\Framework\MockObject\MockObject|Root $root */ + $root = $this->getMockBuilder(Root::class) + ->onlyMethods(['getUser', 'getMountsIn', 'getMount']) + ->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig]) + ->getMock(); + /** @var \PHPUnit\Framework\MockObject\MockObject|FileInfo $folderInfo */ + $folderInfo = $this->getMockBuilder(FileInfo::class) + ->disableOriginalConstructor()->getMock(); + + $baseTime = time(); + $storage = new Temporary(); + $mount = new MountPoint($storage, ''); + + $folderInfo->method('getMountPoint')->willReturn($mount); + $root->method('getMount')->willReturn($mount); + $root->method('getMountsIn')->willReturn([]); + + $cache = $storage->getCache(); + $cache->insert('', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); + $cache->insert('bar', ['size' => 0, 'mtime' => 0, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); + $edited = $cache->put('bar/edited.txt', [ + 'storage_mtime' => $baseTime - 100, + 'mtime' => $baseTime - 100, + 'mimetype' => 'text/plain', + 'size' => 3, + 'permissions' => Constants::PERMISSION_ALL, + ]); + // Uploaded just now, keeping the mtime it had on the uploader's device + $uploaded = $cache->put('bar/uploaded.txt', [ + 'storage_mtime' => $baseTime, + 'mtime' => $baseTime - 30 * 24 * 60 * 60, + 'upload_time' => $baseTime, + 'mimetype' => 'text/plain', + 'size' => 3, + 'permissions' => Constants::PERMISSION_ALL, + ]); + $cache->put('bar/old.txt', [ + 'storage_mtime' => $baseTime - 30 * 24 * 60 * 60, + 'mtime' => $baseTime - 30 * 24 * 60 * 60, + 'mimetype' => 'text/plain', + 'size' => 3, + 'permissions' => Constants::PERMISSION_ALL, + ]); + + $node = new Folder($root, $view, '/bar', $folderInfo); + + $ids = array_map(fn (Node $node): int => (int)$node->getId(), $node->getRecent(5)); + $this->assertEquals([$uploaded, $edited], $ids); + } + public function testRecentFolder(): void { $manager = $this->createMock(Manager::class); $folderPath = '/bar/foo'; From ea5d37f33cfd7f1b24931474597c35ebd3444f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= <14975046+skjnldsv@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:50:08 +0100 Subject: [PATCH 2/3] fix(files): quote the upload time when searching by last activity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last activity order was built as raw SQL, `COALESCE(fe.upload_time, 0)`, so Oracle read the unquoted name as "FE"."UPLOAD_TIME" and failed with ORA-00904. Build it with the function builder, which quotes it. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com> --- lib/private/Files/Cache/SearchBuilder.php | 2 +- tests/lib/Files/Cache/CacheTest.php | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index 43c82035c7172..01ee81af836fb 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -366,7 +366,7 @@ public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders, ?IMe } if ($field === 'last_activity') { - $field = $query->func()->greatest('file.mtime', $query->createFunction('COALESCE(fe.upload_time, 0)')); + $field = $query->func()->greatest('file.mtime', $query->func()->coalesce('fe.upload_time', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))); } } $query->addOrderBy($field, $order->getDirection()); diff --git a/tests/lib/Files/Cache/CacheTest.php b/tests/lib/Files/Cache/CacheTest.php index 57b8229b6d671..28535654b5749 100644 --- a/tests/lib/Files/Cache/CacheTest.php +++ b/tests/lib/Files/Cache/CacheTest.php @@ -14,6 +14,7 @@ use OC\Files\Cache\QuerySearchHelper; use OC\Files\Cache\Wrapper\CacheJail; use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchOrder; use OC\Files\Search\SearchQuery; use OC\Files\Storage\Temporary; use OC\SystemConfig; @@ -25,6 +26,7 @@ use OCP\Files\Cache\ICacheEntry; use OCP\Files\IMimeTypeLoader; use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOrder; use OCP\Files\Storage\IStorage; use OCP\FilesMetadata\IFilesMetadataManager; use OCP\IDBConnection; @@ -633,6 +635,24 @@ public function testSearchByQuery(): void { new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', 100), 10, 0, [], $user))); } + public function testSearchOrderedByLastActivity(): void { + $this->cache->put('folder', ['size' => 0, 'mtime' => 10, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]); + $this->cache->put('folder/edited', ['size' => 1, 'mtime' => 300, 'mimetype' => 'foo/file']); + // Uploaded last, keeping an older mtime from the uploader's device + $this->cache->put('folder/uploaded', ['size' => 1, 'mtime' => 100, 'upload_time' => 400, 'mimetype' => 'foo/file']); + $this->cache->put('folder/old', ['size' => 1, 'mtime' => 200, 'mimetype' => 'foo/file']); + + $results = $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'foo/file'), + 10, + 0, + [new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'last_activity')], + $this->createMock(IUser::class), + )); + + $this->assertSame(['uploaded', 'edited', 'old'], array_map(fn (ICacheEntry $entry): string => $entry->getName(), $results)); + } + public static function movePathProvider(): array { return [ ['folder/foo', 'folder/foobar', ['1', '2']], From 3b77623a551e08da4beda014eb16de503f0378e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= <14975046+skjnldsv@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:54:32 +0100 Subject: [PATCH 3/3] fix(files): use a numeric default for the upload time on Oracle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oracle binds the parameter as text, and COALESCE refuses to mix it with the number column (ORA-00932). The default is a constant, so write it in the query. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com> --- lib/private/Files/Cache/SearchBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index 01ee81af836fb..9a32f5cb93467 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -366,7 +366,7 @@ public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders, ?IMe } if ($field === 'last_activity') { - $field = $query->func()->greatest('file.mtime', $query->func()->coalesce('fe.upload_time', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + $field = $query->func()->greatest('file.mtime', $query->func()->coalesce('fe.upload_time', $query->createFunction('0'))); } } $query->addOrderBy($field, $order->getDirection());