Merge pull request #59216 from nextcloud/backport/58761/stable33

[stable33] feat(recent-files): add recent_files_limit config on files settings
This commit is contained in:
Cristian Scheid 2026-03-26 13:37:32 -03:00 committed by GitHub
commit f70548660a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 89 additions and 10 deletions

View file

@ -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)) {

View file

@ -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,10 @@ class FilesPlugin extends ServerPlugin {
return $node->getFileInfo()->getCreationTime();
});
$propFind->handle(self::LAST_ACTIVITY_PROPERTYNAME, function () use ($node) {
return $node->getFileInfo()->getLastActivity();
});
foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
$propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue);
}

View file

@ -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 $node->getNode()->getLastActivity();
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));
}

View file

@ -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"
},

View file

@ -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,
],

View file

@ -22,6 +22,7 @@ use OCP\Config\ValueType;
*/
class ConfigLexicon implements ILexicon {
public const OVERWRITES_HOME_FOLDERS = 'overwrites_home_folders';
public const RECENT_LIMIT = 'recent_limit';
public function getStrictness(): Strictness {
return Strictness::IGNORE;
@ -37,6 +38,13 @@ class ConfigLexicon implements ILexicon {
lazy: false,
note: 'It will be populated with app IDs of mount providers that overwrite home folders. Currently, only files_external and groupfolders.',
),
new Entry(
self::RECENT_LIMIT,
ValueType::INT,
defaultRaw: 100,
definition: 'Maximum number of files to display on recent files view',
lazy: false,
),
];
}

View file

@ -10,6 +10,7 @@ namespace OCA\Files\Controller;
use OC\Files\FilenameValidator;
use OC\Files\Filesystem;
use OCA\Files\AppInfo\Application;
use OCA\Files\ConfigLexicon;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadSearchPlugins;
use OCA\Files\Event\LoadSidebar;
@ -25,6 +26,7 @@ use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Services\IInitialState;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
@ -62,6 +64,7 @@ class ViewController extends Controller {
private ViewConfig $viewConfig,
private FilenameValidator $filenameValidator,
private IRegistry $twoFactorRegistry,
private IAppConfig $appConfig,
) {
parent::__construct($appName, $request);
}
@ -174,6 +177,7 @@ class ViewController extends Controller {
$this->initialState->provideInitialState('storageStats', $storageInfo);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
$this->initialState->provideInitialState('recent_limit', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_LIMIT, 100));
// File sorting user config
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);

View file

@ -8,12 +8,14 @@ import type { ResponseDataDetailed, SearchResult } from 'webdav'
import { getCurrentUser } from '@nextcloud/auth'
import { Folder, Permission } from '@nextcloud/files'
import { getRecentSearch, getRemoteURL, getRootPath, resultToNode } from '@nextcloud/files/dav'
import { loadState } from '@nextcloud/initial-state'
import logger from '../logger.ts'
import { getPinia } from '../store/index.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
import { client } from './WebdavClient.ts'
const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 14))
const recentLimit = loadState<number>('files', 'recent_limit', 100)
/**
* Get recently changed nodes
@ -41,7 +43,7 @@ export async function getContents(path = '/', options: { signal: AbortSignal }):
const contentsResponse = await client.search('/', {
signal: options.signal,
details: true,
data: getRecentSearch(lastTwoWeeksTimestamp),
data: getRecentSearch(lastTwoWeeksTimestamp, recentLimit),
}) as ResponseDataDetailed<SearchResult>
const contents = contentsResponse.data.results

View file

@ -18,6 +18,7 @@ use OCP\App\IAppManager;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Services\IInitialState;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Diagnostics\IEventLogger;
@ -48,6 +49,7 @@ use Test\TestCase;
class ViewControllerTest extends TestCase {
private ContainerInterface&MockObject $container;
private IAppManager&MockObject $appManager;
private IAppConfig&MockObject $appConfig;
private ICacheFactory&MockObject $cacheFactory;
private IConfig&MockObject $config;
private IEventDispatcher $eventDispatcher;
@ -71,6 +73,7 @@ class ViewControllerTest extends TestCase {
protected function setUp(): void {
parent::setUp();
$this->appManager = $this->createMock(IAppManager::class);
$this->appConfig = $this->createMock(IAppConfig::class);
$this->config = $this->createMock(IConfig::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->initialState = $this->createMock(IInitialState::class);
@ -142,6 +145,7 @@ class ViewControllerTest extends TestCase {
$this->viewConfig,
$filenameValidator,
$this->twoFactorRegistry,
$this->appConfig,
])
->onlyMethods([
'getStorageInfo',
@ -298,11 +302,11 @@ class ViewControllerTest extends TestCase {
'backup_codes' => true,
]);
$invokedCountProvideInitialState = $this->exactly(9);
$invokedCountProvideInitialState = $this->exactly(10);
$this->initialState->expects($invokedCountProvideInitialState)
->method('provideInitialState')
->willReturnCallback(function ($key, $data) use ($invokedCountProvideInitialState): void {
if ($invokedCountProvideInitialState->numberOfInvocations() === 9) {
if ($invokedCountProvideInitialState->numberOfInvocations() === 10) {
$this->assertEquals('isTwoFactorEnabled', $key);
$this->assertTrue($data);
}

View file

@ -154,6 +154,10 @@ class TrashItem implements ITrashItem {
return $this->fileInfo->getUploadTime();
}
public function getLastActivity(): int {
return $this->fileInfo->getLastActivity();
}
public function getParentId(): int {
return $this->fileInfo->getParentId();
}

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -152,7 +152,11 @@ class QuerySearchHelper {
$requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());
$joinExtendedCache = in_array('creation_time', $requestedFields) || in_array('upload_time', $requestedFields);
$orderFields = array_map(fn ($order) => $order->getField(), $searchQuery->getOrder());
$joinExtendedCache = in_array('creation_time', $requestedFields)
|| in_array('upload_time', $requestedFields)
|| in_array('last_activity', $orderFields);
$query = $builder->selectFileCache('file', $joinExtendedCache);

View file

@ -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());
}

View file

@ -379,6 +379,10 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
return (int)$this->data['upload_time'];
}
public function getLastActivity(): int {
return max($this->getUploadTime(), $this->getMTime());
}
public function getParentId(): int {
return $this->data['parent'] ?? -1;
}

View file

@ -546,6 +546,13 @@ class LazyFolder implements Folder {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getLastActivity(): int {
return $this->__call(__FUNCTION__, func_get_args());
}
public function getRelativePath($path) {
return PathHelper::getRelativePath($this->getPath(), $path);
}

View file

@ -475,6 +475,10 @@ class Node implements INode {
return $this->getFileInfo()->getUploadTime();
}
public function getLastActivity(): int {
return $this->getFileInfo()->getLastActivity();
}
public function getParentId(): int {
return $this->fileInfo->getParentId();
}

View file

@ -58,6 +58,8 @@ class SearchOrder implements ISearchOrder {
return $a->getId() <=> $b->getId();
case 'permissions':
return $a->getPermissions() <=> $b->getPermissions();
case 'last_activity':
return $a->getLastActivity() <=> $b->getLastActivity();
default:
return 0;
}

View file

@ -282,6 +282,16 @@ interface FileInfo {
*/
public function getUploadTime(): int;
/**
* Get the last activity date as unix timestamp
*
* Last activity is the more recent of the upload time and the modification time
*
* @return int
* @since 33.0.1
*/
public function getLastActivity(): int;
/**
* Get the fileid or the parent folder
* or -1 if this item has no parent folder (because it is the root)

View file

@ -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"
},