perf: Allow filtering the directory content by mimetype

Signed-off-by: Carl Schwan <carlschwan@kde.org>
This commit is contained in:
Carl Schwan 2026-01-28 23:54:17 +01:00
parent 935cd2910f
commit 9741f5f17d
No known key found for this signature in database
GPG key ID: 02325448204E452A
15 changed files with 132 additions and 121 deletions

View file

@ -249,7 +249,7 @@ class ApiController extends Controller {
* @param \OCP\Files\Node[] $nodes
* @param int $depth The depth to traverse into the contents of each node
*/
private function getChildren(array $nodes, int $depth = 1, int $currentDepth = 0): array {
private function getChildren(array $nodes, int $depth = 1, int $currentDepth = 0, string $mimeTypeFilter = ''): array {
if ($currentDepth >= $depth) {
return [];
}
@ -264,7 +264,7 @@ class ApiController extends Controller {
$entry = [
'id' => $node->getId(),
'basename' => $basename,
'children' => $this->getChildren($node->getDirectoryListing(), $depth, $currentDepth + 1),
'children' => $this->getChildren($node->getDirectoryListing($mimeTypeFilter), $depth, $currentDepth + 1),
];
$displayName = $node->getName();
if ($basename !== $displayName) {
@ -308,8 +308,8 @@ class ApiController extends Controller {
'message' => $this->l10n->t('Invalid folder path'),
], Http::STATUS_BAD_REQUEST);
}
$nodes = $node->getDirectoryListing();
$tree = $this->getChildren($nodes, $depth);
$nodes = $node->getDirectoryListing('httpd/unix-directory');
$tree = $this->getChildren($nodes, $depth, 0, 'httpd/unix-directory');
} catch (NotFoundException $e) {
return new JSONResponse([
'message' => $this->l10n->t('Folder not found'),

View file

@ -41,8 +41,8 @@ class Cache extends \OC\Files\Cache\Cache {
return $result;
}
public function getFolderContentsById($fileId) {
$results = parent::getFolderContentsById($fileId);
public function getFolderContentsById($fileId, ?string $mimeTypeFilter = null): array {
$results = parent::getFolderContentsById($fileId, $mimeTypeFilter);
foreach ($results as &$file) {
$file['displayname_owner'] = $this->cloudId->getDisplayId();
}

View file

@ -309,7 +309,7 @@ function execute_tests {
if [ ! -z "$USEDOCKER" ] ; then
echo "Fire up the postgres docker"
DOCKER_CONTAINER_ID=$(docker run -e POSTGRES_DB="$DATABASENAME" -e POSTGRES_USER="$DATABASEUSER" -e POSTGRES_PASSWORD=owncloud -d postgres)
DATABASEHOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "$DOCKER_CONTAINER_ID")
DATABASEHOST=$(docker inspect --format="{{ range .NetworkSettings.Networks }}{{ .IPAddress }}{{ end }}" "$DOCKER_CONTAINER_ID")
echo "Waiting for Postgres initialisation ..."

View file

@ -218,7 +218,7 @@ class Cache implements ICache {
* @param int $fileId the file id of the folder
* @return ICacheEntry[]
*/
public function getFolderContentsById($fileId) {
public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = null) {
if ($fileId > -1) {
$query = $this->getQueryBuilder();
$query->selectFileCache()
@ -226,13 +226,22 @@ class Cache implements ICache {
->whereStorageId($this->getNumericStorageId())
->orderBy('name', 'ASC');
if ($mimeTypeFilter !== null) {
$mimetype = $this->mimetypeLoader->getId($mimeTypeFilter);
if (str_contains($mimeTypeFilter, '/')) {
$query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimetype)));
} else {
$query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimetype)));
}
}
$metadataQuery = $query->selectMetadata();
$result = $query->executeQuery();
$files = $result->fetchAll();
$result->closeCursor();
return array_map(function (array $data) use ($metadataQuery) {
return array_map(function (array $data) use ($metadataQuery): ICacheEntry {
$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
return self::cacheEntryFromData($data, $this->mimetypeLoader);
}, $files);

View file

@ -26,12 +26,11 @@ class FailedCache implements ICache {
) {
}
public function getNumericStorageId() {
public function getNumericStorageId(): int {
return -1;
}
public function get($file) {
public function get($file): false|ICacheEntry {
if ($file === '') {
return new CacheEntry([
'fileid' => -1,
@ -46,11 +45,11 @@ class FailedCache implements ICache {
}
}
public function getFolderContents($folder) {
public function getFolderContents($folder): array {
return [];
}
public function getFolderContentsById($fileId) {
public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = null): array {
return [];
}
@ -63,15 +62,15 @@ class FailedCache implements ICache {
public function update($id, array $data) {
}
public function getId($file) {
public function getId($file): int {
return -1;
}
public function getParentId($file) {
public function getParentId($file): int {
return -1;
}
public function inCache($file) {
public function inCache($file): bool {
return false;
}

View file

@ -102,9 +102,9 @@ class CacheWrapper extends Cache {
* @param int $fileId the file id of the folder
* @return array
*/
public function getFolderContentsById($fileId) {
$results = $this->getCache()->getFolderContentsById($fileId);
return array_map([$this, 'formatCacheEntry'], $results);
public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = null) {
$results = $this->getCache()->getFolderContentsById($fileId, $mimeTypeFilter);
return array_map($this->formatCacheEntry(...), $results);
}
/**

View file

@ -663,14 +663,14 @@ class Filesystem {
}
/**
* get the content of a directory
* Get the content of a directory.
*
* @param string $directory path under datadirectory
* @param string $mimetype_filter limit returned content to this mimetype or mimepart
* @param string $mimeTypeFilter limit returned content to this mimetype or mimepart
* @return FileInfo[]
*/
public static function getDirectoryContent($directory, $mimetype_filter = '') {
return self::$defaultInstance->getDirectoryContent($directory, $mimetype_filter);
public static function getDirectoryContent($directory, string $mimeTypeFilter = '') {
return self::$defaultInstance->getDirectoryContent($directory, $mimeTypeFilter);
}
/**

View file

@ -76,16 +76,11 @@ class Folder extends Node implements IFolder {
return str_starts_with($node->getPath(), $this->path . '/');
}
/**
* get the content of this directory
*
* @return Node[]
* @throws \OCP\Files\NotFoundException
*/
public function getDirectoryListing() {
$folderContent = $this->view->getDirectoryContent($this->path, '', $this->getFileInfo(false));
#[Override]
public function getDirectoryListing(?string $mimetypeFilter = null): array {
$folderContent = $this->view->getDirectoryContent($this->path, $mimetypeFilter, $this->getFileInfo(false));
return array_map(function (FileInfo $info) {
return array_map(function (FileInfo $info): Node {
if ($info->getMimetype() === FileInfo::MIMETYPE_FOLDER) {
return new Folder($this->root, $this->view, $info->getPath(), $info, $this);
} else {

View file

@ -415,10 +415,8 @@ class LazyFolder implements Folder {
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* @inheritDoc
*/
public function getDirectoryListing() {
#[Override]
public function getDirectoryListing(?string $mimetypeFilter = null): array {
return $this->__call(__FUNCTION__, func_get_args());
}

View file

@ -8,6 +8,7 @@
namespace OC\Files\Node;
use OCP\Files\NotFoundException;
use Override;
class NonExistingFolder extends Folder {
/**
@ -118,7 +119,8 @@ class NonExistingFolder extends Folder {
throw new NotFoundException();
}
public function getDirectoryListing() {
#[Override]
public function getDirectoryListing(?string $mimetypeFilter = null): never {
throw new NotFoundException();
}

View file

@ -1481,15 +1481,20 @@ class View {
* get the content of a directory
*
* @param string $directory path under datadirectory
* @param string $mimetype_filter limit returned content to this mimetype or mimepart
* @param ?non-empty-string $mimeTypeFilter limit returned content to this mimetype or mimepart
* @return FileInfo[]
*/
public function getDirectoryContent($directory, $mimetype_filter = '', ?\OCP\Files\FileInfo $directoryInfo = null) {
public function getDirectoryContent(string $directory, ?string $mimeTypeFilter = null, ?\OCP\Files\FileInfo $directoryInfo = null) {
$this->assertPathLength($directory);
if (!Filesystem::isValidPath($directory)) {
return [];
}
/** @psalm-suppress TypeDoesNotContainType For legacy compatibility */
if ($mimeTypeFilter === '') {
$mimeTypeFilter = null;
}
$path = $this->getAbsolutePath($directory);
$path = Filesystem::normalizePath($path);
$mount = $this->getMount($directory);
@ -1516,7 +1521,7 @@ class View {
}
$folderId = $data->getId();
$contents = $cache->getFolderContentsById($folderId); //TODO: mimetype_filter
$contents = $cache->getFolderContentsById($folderId, $mimeTypeFilter);
$sharingDisabled = \OCP\Util::isSharingDisabledForUser();
$permissionsMask = ~\OCP\Constants::PERMISSION_SHARE;
@ -1577,79 +1582,79 @@ class View {
$rootEntry = $subCache->get('');
}
if ($rootEntry && ($rootEntry->getPermissions() & Constants::PERMISSION_READ)) {
$relativePath = trim(substr($mountPoint, $dirLength), '/');
if ($pos = strpos($relativePath, '/')) {
//mountpoint inside subfolder add size to the correct folder
$entryName = substr($relativePath, 0, $pos);
if (!$rootEntry || !($rootEntry->getPermissions() & Constants::PERMISSION_READ)) {
continue;
}
// Create parent folders if the mountpoint is inside a subfolder that doesn't exist yet
if (!isset($files[$entryName])) {
try {
[$storage, ] = $this->resolvePath($path . '/' . $entryName);
// make sure we can create the mountpoint folder, even if the user has a quota of 0
if ($storage->instanceOfStorage(Quota::class)) {
$storage->enableQuota(false);
}
if ($this->mkdir($path . '/' . $entryName) !== false) {
$info = $this->getFileInfo($path . '/' . $entryName);
if ($info !== false) {
$files[$entryName] = $info;
}
}
if ($storage->instanceOfStorage(Quota::class)) {
$storage->enableQuota(true);
}
} catch (\Exception $e) {
// Creating the parent folder might not be possible, for example due to a lack of permissions.
$this->logger->debug('Failed to create non-existent parent', ['exception' => $e, 'path' => $path . '/' . $entryName]);
}
}
if (isset($files[$entryName])) {
$files[$entryName]->addSubEntry($rootEntry, $mountPoint);
}
} else { //mountpoint in this folder, add an entry for it
$rootEntry['name'] = $relativePath;
$rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
$permissions = $rootEntry['permissions'];
// do not allow renaming/deleting the mount point if they are not shared files/folders
// for shared files/folders we use the permissions given by the owner
if ($mount instanceof MoveableMount) {
$rootEntry['permissions'] = $permissions | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE;
} else {
$rootEntry['permissions'] = $permissions & (Constants::PERMISSION_ALL - (Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE));
}
$rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/
// if sharing was disabled for the user we remove the share permissions
if ($sharingDisabled) {
$rootEntry['permissions'] = $rootEntry['permissions'] & ~Constants::PERMISSION_SHARE;
}
$ownerId = $subStorage->getOwner('');
if ($ownerId !== false) {
$owner = $this->getUserObjectForOwner($ownerId);
} else {
$owner = null;
}
$files[$rootEntry->getName()] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
if ($mimeTypeFilter !== null) {
if (strpos($mimeTypeFilter, '/') !== false && $rootEntry['mimetype'] !== $mimeTypeFilter) {
continue;
} elseif (strpos($mimeTypeFilter, '/') === false && $rootEntry['mimepart'] !== $mimeTypeFilter) {
continue;
}
}
}
}
if ($mimetype_filter) {
$files = array_filter($files, function (FileInfo $file) use ($mimetype_filter) {
if (strpos($mimetype_filter, '/')) {
return $file->getMimetype() === $mimetype_filter;
} else {
return $file->getMimePart() === $mimetype_filter;
$relativePath = trim(substr($mountPoint, $dirLength), '/');
if ($pos = strpos($relativePath, '/')) {
//mountpoint inside subfolder add size to the correct folder
$entryName = substr($relativePath, 0, $pos);
// Create parent folders if the mountpoint is inside a subfolder that doesn't exist yet
if (!isset($files[$entryName])) {
try {
[$storage, ] = $this->resolvePath($path . '/' . $entryName);
// make sure we can create the mountpoint folder, even if the user has a quota of 0
if ($storage->instanceOfStorage(Quota::class)) {
$storage->enableQuota(false);
}
if ($this->mkdir($path . '/' . $entryName) !== false) {
$info = $this->getFileInfo($path . '/' . $entryName);
if ($info !== false) {
$files[$entryName] = $info;
}
}
if ($storage->instanceOfStorage(Quota::class)) {
$storage->enableQuota(true);
}
} catch (\Exception $e) {
// Creating the parent folder might not be possible, for example due to a lack of permissions.
$this->logger->debug('Failed to create non-existent parent', ['exception' => $e, 'path' => $path . '/' . $entryName]);
}
}
if (isset($files[$entryName])) {
$files[$entryName]->addSubEntry($rootEntry, $mountPoint);
}
} else { //mountpoint in this folder, add an entry for it
$rootEntry['name'] = $relativePath;
$rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file';
$permissions = $rootEntry['permissions'];
// do not allow renaming/deleting the mount point if they are not shared files/folders
// for shared files/folders we use the permissions given by the owner
if ($mount instanceof MoveableMount) {
$rootEntry['permissions'] = $permissions | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE;
} else {
$rootEntry['permissions'] = $permissions & (Constants::PERMISSION_ALL - (Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE));
}
$rootEntry['path'] = substr(Filesystem::normalizePath($path . '/' . $rootEntry['name']), strlen($user) + 2); // full path without /$user/
// if sharing was disabled for the user we remove the share permissions
if ($sharingDisabled) {
$rootEntry['permissions'] = $rootEntry['permissions'] & ~Constants::PERMISSION_SHARE;
}
$ownerId = $subStorage->getOwner('');
if ($ownerId !== false) {
$owner = $this->getUserObjectForOwner($ownerId);
} else {
$owner = null;
}
$files[$rootEntry->getName()] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount, $owner);
}
});
}
}
return array_values($files);

View file

@ -20,11 +20,11 @@ use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchQuery;
class NullCache implements ICache {
public function getNumericStorageId() {
public function getNumericStorageId(): int {
return -1;
}
public function get($file) {
public function get($file): false|ICacheEntry {
if ($file !== '') {
return false;
}
@ -44,23 +44,23 @@ class NullCache implements ICache {
]);
}
public function getFolderContents($folder) {
public function getFolderContents($folder): array {
return [];
}
public function getFolderContentsById($fileId) {
public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = null): array {
return [];
}
public function put($file, array $data) {
public function put($file, array $data): never {
throw new ForbiddenException('This request is not allowed to access the filesystem');
}
public function insert($file, array $data) {
public function insert($file, array $data): never {
throw new ForbiddenException('This request is not allowed to access the filesystem');
}
public function update($id, array $data) {
public function update($id, array $data): never {
throw new ForbiddenException('This request is not allowed to access the filesystem');
}

View file

@ -77,10 +77,12 @@ interface ICache {
* Only returns files one level deep, no recursion
*
* @param int $fileId the file id of the folder
* @param ?non-empty-string $mimeTypeFilter The mimetype or mimepart for which the content should be filtered
* @return ICacheEntry[]
* @since 9.0.0
* @since 34.0.0 The $mimetypeFilter was added.
*/
public function getFolderContentsById($fileId);
public function getFolderContentsById(int $fileId, ?string $mimeTypeFilter = null);
/**
* store meta data for a file or folder

View file

@ -46,13 +46,14 @@ interface Folder extends Node {
public function isSubNode($node);
/**
* get the content of this directory
* Get the content of this directory.
*
* @param ?non-empty-string $mimetypeFilter Limit the returned content to this mimetype or mimepart
* @throws \OCP\Files\NotFoundException
* @return \OCP\Files\Node[]
* @since 6.0.0
*/
public function getDirectoryListing();
public function getDirectoryListing(?string $mimetypeFilter = null): array;
/**
* Get the node at $path

View file

@ -248,7 +248,7 @@ class DecryptAllTest extends TestCase {
->method('getDirectoryContent')
->willReturnMap([
[
'/user1/files', '', null,
'/user1/files', null, null,
[
new FileInfo('path', $storage, 'intPath', ['name' => 'foo', 'type' => 'dir'], null),
new FileInfo('path', $storage, 'intPath', ['name' => 'bar', 'type' => 'file', 'encrypted' => true], null),
@ -256,7 +256,7 @@ class DecryptAllTest extends TestCase {
],
],
[
'/user1/files/foo', '', null,
'/user1/files/foo', null, null,
[
new FileInfo('path', $storage, 'intPath', ['name' => 'subfile', 'type' => 'file', 'encrypted' => true], null)
],