Skip to content

Commit

Permalink
Merge pull request #47122 from nextcloud/feat/limited-depth-tree
Browse files Browse the repository at this point in the history
feat(files): Load limited depth tree
  • Loading branch information
Pytal authored Aug 8, 2024
2 parents b30054a + 36c23b2 commit 8c0bece
Show file tree
Hide file tree
Showing 118 changed files with 393 additions and 266 deletions.
88 changes: 43 additions & 45 deletions apps/files/lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
*/
namespace OCA\Files\Controller;

use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
use OC\Files\Node\Node;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
use OCA\Files\ResponseDefinitions;
use OCA\Files\Service\TagService;
use OCA\Files\Service\UserConfig;
Expand All @@ -29,12 +26,10 @@
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\Search\ISearchComparison;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
Expand Down Expand Up @@ -234,75 +229,78 @@ public function getRecentFiles() {
}

/**
* @param Folder[] $folders
* @param \OCP\Files\Node[] $nodes
* @param int $depth The depth to traverse into the contents of each node
*/
private function getTree(array $folders): array {
$user = $this->userSession->getUser();
if (!($user instanceof IUser)) {
throw new NotLoggedInException();
private function getChildren(array $nodes, int $depth = 1, int $currentDepth = 0): array {
if ($currentDepth >= $depth) {
return [];
}

$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$tree = [];
foreach ($folders as $folder) {
$path = $userFolder->getRelativePath($folder->getPath());
if ($path === null) {
$children = [];
foreach ($nodes as $node) {
if (!($node instanceof Folder)) {
continue;
}
$pathBasenames = explode('/', trim($path, '/'));
$current = &$tree;
foreach ($pathBasenames as $basename) {
if (!isset($current['children'][$basename])) {
$current['children'][$basename] = [
'id' => $folder->getId(),
];
$displayName = $folder->getName();
if ($displayName !== $basename) {
$current['children'][$basename]['displayName'] = $displayName;
}
}
$current = &$current['children'][$basename];

$basename = basename($node->getPath());
$entry = [
'id' => $node->getId(),
'basename' => $basename,
'children' => $this->getChildren($node->getDirectoryListing(), $depth, $currentDepth + 1),
];
$displayName = $node->getName();
if ($basename !== $displayName) {
$entry['displayName'] = $displayName;
}
$children[] = $entry;
}
return $tree['children'] ?? $tree;
return $children;
}

/**
* Returns the folder tree of the user
*
* @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
* @param string $path The path relative to the user folder
* @param int $depth The depth of the tree
*
* @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Folder tree returned successfully
* 400: Invalid folder path
* 401: Unauthorized
* 404: Folder not found
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')]
public function getFolderTree(): JSONResponse {
public function getFolderTree(string $path = '/', int $depth = 1): JSONResponse {
$user = $this->userSession->getUser();
if (!($user instanceof IUser)) {
return new JSONResponse([
'message' => $this->l10n->t('Failed to authorize'),
], Http::STATUS_UNAUTHORIZED);
}

$userFolder = $this->rootFolder->getUserFolder($user->getUID());
try {
$searchQuery = new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE),
0,
0,
[],
$user,
false,
);
/** @var Folder[] $folders */
$folders = $userFolder->search($searchQuery);
$tree = $this->getTree($folders);
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$userFolderPath = $userFolder->getPath();
$fullPath = implode('/', [$userFolderPath, trim($path, '/')]);
$node = $this->rootFolder->get($fullPath);
if (!($node instanceof Folder)) {
return new JSONResponse([
'message' => $this->l10n->t('Invalid folder path'),
], Http::STATUS_BAD_REQUEST);
}
$nodes = $node->getDirectoryListing();
$tree = $this->getChildren($nodes, $depth);
} catch (NotFoundException $e) {
return new JSONResponse([
'message' => $this->l10n->t('Folder not found'),
], Http::STATUS_NOT_FOUND);
} catch (Throwable $th) {
$this->logger->error($th->getMessage(), ['exception' => $th]);
$tree = [];
}
return new JSONResponse($tree, Http::STATUS_OK, [], JSON_FORCE_OBJECT);
return new JSONResponse($tree);
}

/**
Expand Down
9 changes: 4 additions & 5 deletions apps/files/lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@
* type: string,
* }
*
* @psalm-type FilesFolderTreeNode = array{
* @psalm-type FilesFolderTree = list<array{
* id: int,
* basename: string,
* displayName?: string,
* children?: array<string, array{}>,
* }
*
* @psalm-type FilesFolderTree = array<string, FilesFolderTreeNode>
* children: list<array{}>,
* }>
*
*/
class ResponseDefinitions {
Expand Down
105 changes: 83 additions & 22 deletions apps/files/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,28 +100,30 @@
}
},
"FolderTree": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/FolderTreeNode"
}
},
"FolderTreeNode": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"displayName": {
"type": "string"
},
"children": {
"type": "object",
"additionalProperties": {
"type": "object"
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"basename",
"children"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"basename": {
"type": "string"
},
"displayName": {
"type": "string"
},
"children": {
"type": "array",
"items": {
"type": "object"
}
}
}
}
Expand Down Expand Up @@ -1971,6 +1973,29 @@
"basic_auth": []
}
],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"default": "/",
"description": "The path relative to the user folder"
},
"depth": {
"type": "integer",
"format": "int64",
"default": 1,
"description": "The depth of the tree"
}
}
}
}
}
},
"parameters": [
{
"name": "OCS-APIRequest",
Expand Down Expand Up @@ -2011,6 +2036,42 @@
}
}
}
},
"400": {
"description": "Invalid folder path",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"404": {
"description": "Folder not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions apps/files/src/components/FilesNavigationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
:key="view.id"
class="files-navigation__item"
allow-collapse
:loading="view.loading"
:data-cy-files-navigation-item="view.id"
:exact="useExactRouteMatching(view)"
:icon="view.iconClass"
Expand All @@ -17,11 +18,14 @@
:pinned="view.sticky"
:to="generateToNavigation(view)"
:style="style"
@update:open="onToggleExpand(view)">
@update:open="(open) => onOpen(open, view)">
<template v-if="view.icon" #icon>
<NcIconSvgWrapper :svg="view.icon" />
</template>

<!-- Hack to force the collapse icon to be displayed -->
<li v-if="view.loadChildViews && !view.loaded" style="display: none" />

<!-- Recursively nest child views -->
<FilesNavigationItem v-if="hasChildViews(view)"
:parent="view"
Expand Down Expand Up @@ -142,14 +146,18 @@ export default defineComponent({
/**
* Expand/collapse a a view with children and permanently
* save this setting in the server.
* @param view View to toggle
* @param open True if open
* @param view View
*/
onToggleExpand(view: View) {
async onOpen(open: boolean, view: View) {
// Invert state
const isExpanded = this.isExpanded(view)
// Update the view expanded state, might not be necessary
view.expanded = !isExpanded
this.viewConfigStore.update(view.id, 'expanded', !isExpanded)
if (open && view.loadChildViews) {
await view.loadChildViews(view)
}
},
/**
Expand Down
2 changes: 2 additions & 0 deletions apps/files/src/composables/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { View } from '@nextcloud/files'
import type { ShallowRef } from 'vue'

import { getNavigation } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
import { onMounted, onUnmounted, shallowRef, triggerRef } from 'vue'

/**
Expand Down Expand Up @@ -35,6 +36,7 @@ export function useNavigation() {
onMounted(() => {
navigation.addEventListener('update', onUpdateViews)
navigation.addEventListener('updateActive', onUpdateActive)
subscribe('files:navigation:updated', onUpdateViews)
})
onUnmounted(() => {
navigation.removeEventListener('update', onUpdateViews)
Expand Down
Loading

0 comments on commit 8c0bece

Please sign in to comment.