Skip to content

Commit ef7d830

Browse files
authored
Merge pull request #46596 from nextcloud/feat/folder-tree
feat: Navigate via folder tree
2 parents 4488714 + 7f6d6d9 commit ef7d830

40 files changed

+763
-160
lines changed

apps/files/appinfo/routes.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
'url' => '/api/v1/views/{view}/{key}',
6161
'verb' => 'PUT'
6262
],
63+
[
64+
'name' => 'Api#setViewConfig',
65+
'url' => '/api/v1/views',
66+
'verb' => 'PUT'
67+
],
6368
[
6469
'name' => 'Api#getViewConfigs',
6570
'url' => '/api/v1/views',

apps/files/lib/AppInfo/Application.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
4242
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
4343
use OCP\Files\Events\Node\NodeCopiedEvent;
44+
use OCP\Files\IRootFolder;
4445
use OCP\IConfig;
46+
use OCP\IL10N;
4547
use OCP\IPreview;
4648
use OCP\IRequest;
4749
use OCP\IServerContainer;
@@ -53,6 +55,7 @@
5355
use OCP\Share\IManager as IShareManager;
5456
use OCP\Util;
5557
use Psr\Container\ContainerInterface;
58+
use Psr\Log\LoggerInterface;
5659

5760
class Application extends App implements IBootstrap {
5861
public const APP_ID = 'files';
@@ -80,6 +83,9 @@ public function register(IRegistrationContext $context): void {
8083
$server->getUserFolder(),
8184
$c->get(UserConfig::class),
8285
$c->get(ViewConfig::class),
86+
$c->get(IL10N::class),
87+
$c->get(IRootFolder::class),
88+
$c->get(LoggerInterface::class),
8389
);
8490
});
8591

apps/files/lib/Controller/ApiController.php

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
*/
88
namespace OCA\Files\Controller;
99

10+
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
1011
use OC\Files\Node\Node;
12+
use OC\Files\Search\SearchComparison;
13+
use OC\Files\Search\SearchQuery;
14+
use OCA\Files\ResponseDefinitions;
1115
use OCA\Files\Service\TagService;
1216
use OCA\Files\Service\UserConfig;
1317
use OCA\Files\Service\ViewConfig;
1418
use OCP\AppFramework\Controller;
1519
use OCP\AppFramework\Http;
20+
use OCP\AppFramework\Http\Attribute\ApiRoute;
1621
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
1722
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
1823
use OCP\AppFramework\Http\Attribute\OpenAPI;
@@ -24,48 +29,44 @@
2429
use OCP\AppFramework\Http\JSONResponse;
2530
use OCP\AppFramework\Http\Response;
2631
use OCP\AppFramework\Http\StreamResponse;
32+
use OCP\Files\Cache\ICacheEntry;
2733
use OCP\Files\File;
2834
use OCP\Files\Folder;
35+
use OCP\Files\IRootFolder;
2936
use OCP\Files\NotFoundException;
37+
use OCP\Files\Search\ISearchComparison;
3038
use OCP\IConfig;
39+
use OCP\IL10N;
3140
use OCP\IPreview;
3241
use OCP\IRequest;
42+
use OCP\IUser;
3343
use OCP\IUserSession;
3444
use OCP\Share\IManager;
3545
use OCP\Share\IShare;
46+
use Psr\Log\LoggerInterface;
47+
use Throwable;
3648

3749
/**
50+
* @psalm-import-type FilesFolderTree from ResponseDefinitions
51+
*
3852
* @package OCA\Files\Controller
3953
*/
4054
class ApiController extends Controller {
41-
private TagService $tagService;
42-
private IManager $shareManager;
43-
private IPreview $previewManager;
44-
private IUserSession $userSession;
45-
private IConfig $config;
46-
private ?Folder $userFolder;
47-
private UserConfig $userConfig;
48-
private ViewConfig $viewConfig;
49-
5055
public function __construct(string $appName,
5156
IRequest $request,
52-
IUserSession $userSession,
53-
TagService $tagService,
54-
IPreview $previewManager,
55-
IManager $shareManager,
56-
IConfig $config,
57-
?Folder $userFolder,
58-
UserConfig $userConfig,
59-
ViewConfig $viewConfig) {
57+
private IUserSession $userSession,
58+
private TagService $tagService,
59+
private IPreview $previewManager,
60+
private IManager $shareManager,
61+
private IConfig $config,
62+
private ?Folder $userFolder,
63+
private UserConfig $userConfig,
64+
private ViewConfig $viewConfig,
65+
private IL10N $l10n,
66+
private IRootFolder $rootFolder,
67+
private LoggerInterface $logger,
68+
) {
6069
parent::__construct($appName, $request);
61-
$this->userSession = $userSession;
62-
$this->tagService = $tagService;
63-
$this->previewManager = $previewManager;
64-
$this->shareManager = $shareManager;
65-
$this->config = $config;
66-
$this->userFolder = $userFolder;
67-
$this->userConfig = $userConfig;
68-
$this->viewConfig = $viewConfig;
6970
}
7071

7172
/**
@@ -232,6 +233,77 @@ public function getRecentFiles() {
232233
return new DataResponse(['files' => $files]);
233234
}
234235

236+
/**
237+
* @param Folder[] $folders
238+
*/
239+
private function getTree(array $folders): array {
240+
$user = $this->userSession->getUser();
241+
if (!($user instanceof IUser)) {
242+
throw new NotLoggedInException();
243+
}
244+
245+
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
246+
$tree = [];
247+
foreach ($folders as $folder) {
248+
$path = $userFolder->getRelativePath($folder->getPath());
249+
if ($path === null) {
250+
continue;
251+
}
252+
$pathBasenames = explode('/', trim($path, '/'));
253+
$current = &$tree;
254+
foreach ($pathBasenames as $basename) {
255+
if (!isset($current['children'][$basename])) {
256+
$current['children'][$basename] = [
257+
'id' => $folder->getId(),
258+
];
259+
$displayName = $folder->getName();
260+
if ($displayName !== $basename) {
261+
$current['children'][$basename]['displayName'] = $displayName;
262+
}
263+
}
264+
$current = &$current['children'][$basename];
265+
}
266+
}
267+
return $tree['children'] ?? $tree;
268+
}
269+
270+
/**
271+
* Returns the folder tree of the user
272+
*
273+
* @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
274+
*
275+
* 200: Folder tree returned successfully
276+
* 401: Unauthorized
277+
*/
278+
#[NoAdminRequired]
279+
#[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')]
280+
public function getFolderTree(): JSONResponse {
281+
$user = $this->userSession->getUser();
282+
if (!($user instanceof IUser)) {
283+
return new JSONResponse([
284+
'message' => $this->l10n->t('Failed to authorize'),
285+
], Http::STATUS_UNAUTHORIZED);
286+
}
287+
288+
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
289+
try {
290+
$searchQuery = new SearchQuery(
291+
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE),
292+
0,
293+
0,
294+
[],
295+
$user,
296+
false,
297+
);
298+
/** @var Folder[] $folders */
299+
$folders = $userFolder->search($searchQuery);
300+
$tree = $this->getTree($folders);
301+
} catch (Throwable $th) {
302+
$this->logger->error($th->getMessage(), ['exception' => $th]);
303+
$tree = [];
304+
}
305+
return new JSONResponse($tree, Http::STATUS_OK, [], JSON_FORCE_OBJECT);
306+
}
235307

236308
/**
237309
* Returns the current logged-in user's storage stats.

apps/files/lib/ResponseDefinitions.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@
3838
* content: string,
3939
* type: string,
4040
* }
41+
*
42+
* @psalm-type FilesFolderTreeNode = array{
43+
* id: int,
44+
* displayName?: string,
45+
* children?: array<string, array{}>,
46+
* }
47+
*
48+
* @psalm-type FilesFolderTree = array<string, FilesFolderTreeNode>
49+
*
4150
*/
4251
class ResponseDefinitions {
4352
}

apps/files/lib/Service/UserConfig.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class UserConfig {
4242
'default' => false,
4343
'allowed' => [true, false],
4444
],
45+
[
46+
// Whether to show the folder tree
47+
'key' => 'folder_tree',
48+
'default' => true,
49+
'allowed' => [true, false],
50+
],
4551
];
4652

4753
protected IConfig $config;
@@ -108,7 +114,7 @@ public function setConfig(string $key, $value): void {
108114
if (!in_array($key, $this->getAllowedConfigKeys())) {
109115
throw new \InvalidArgumentException('Unknown config key');
110116
}
111-
117+
112118
if (!in_array($value, $this->getAllowedConfigValues($key))) {
113119
throw new \InvalidArgumentException('Invalid config value');
114120
}

apps/files/openapi.json

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,33 @@
9999
}
100100
}
101101
},
102+
"FolderTree": {
103+
"type": "object",
104+
"additionalProperties": {
105+
"$ref": "#/components/schemas/FolderTreeNode"
106+
}
107+
},
108+
"FolderTreeNode": {
109+
"type": "object",
110+
"required": [
111+
"id"
112+
],
113+
"properties": {
114+
"id": {
115+
"type": "integer",
116+
"format": "int64"
117+
},
118+
"displayName": {
119+
"type": "string"
120+
},
121+
"children": {
122+
"type": "object",
123+
"additionalProperties": {
124+
"type": "object"
125+
}
126+
}
127+
}
128+
},
102129
"OCSMeta": {
103130
"type": "object",
104131
"required": [
@@ -1928,6 +1955,65 @@
19281955
}
19291956
}
19301957
}
1958+
},
1959+
"/ocs/v2.php/apps/files/api/v1/folder-tree": {
1960+
"get": {
1961+
"operationId": "api-get-folder-tree",
1962+
"summary": "Returns the folder tree of the user",
1963+
"tags": [
1964+
"api"
1965+
],
1966+
"security": [
1967+
{
1968+
"bearer_auth": []
1969+
},
1970+
{
1971+
"basic_auth": []
1972+
}
1973+
],
1974+
"parameters": [
1975+
{
1976+
"name": "OCS-APIRequest",
1977+
"in": "header",
1978+
"description": "Required to be true for the API request to pass",
1979+
"required": true,
1980+
"schema": {
1981+
"type": "boolean",
1982+
"default": true
1983+
}
1984+
}
1985+
],
1986+
"responses": {
1987+
"200": {
1988+
"description": "Folder tree returned successfully",
1989+
"content": {
1990+
"application/json": {
1991+
"schema": {
1992+
"$ref": "#/components/schemas/FolderTree"
1993+
}
1994+
}
1995+
}
1996+
},
1997+
"401": {
1998+
"description": "Unauthorized",
1999+
"content": {
2000+
"application/json": {
2001+
"schema": {
2002+
"type": "object",
2003+
"required": [
2004+
"message"
2005+
],
2006+
"properties": {
2007+
"message": {
2008+
"type": "string"
2009+
}
2010+
}
2011+
}
2012+
}
2013+
}
2014+
}
2015+
}
2016+
}
19312017
}
19322018
},
19332019
"tags": []

apps/files/src/components/BreadCrumbs.vue

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,11 @@ export default defineComponent({
109109
return this.dirs.map((dir: string, index: number) => {
110110
const source = this.getFileSourceFromPath(dir)
111111
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
112-
const to = { ...this.$route, params: { node: node?.fileid }, query: { dir } }
113112
return {
114113
dir,
115114
exact: true,
116115
name: this.getDirDisplayName(dir),
117-
to,
116+
to: this.getTo(dir, node),
118117
// disable drop on current directory
119118
disableDrop: index === this.dirs.length - 1,
120119
}
@@ -163,6 +162,27 @@ export default defineComponent({
163162
return node?.displayname || basename(path)
164163
},
165164
165+
getTo(dir: string, node?: Node): Record<string, unknown> {
166+
if (dir === '/') {
167+
return {
168+
...this.$route,
169+
params: { view: this.currentView?.id },
170+
query: {},
171+
}
172+
}
173+
if (node === undefined) {
174+
return {
175+
...this.$route,
176+
query: { dir },
177+
}
178+
}
179+
return {
180+
...this.$route,
181+
params: { fileid: String(node.fileid) },
182+
query: { dir: node.path },
183+
}
184+
},
185+
166186
onClick(to) {
167187
if (to?.query?.dir === this.$route.query.dir) {
168188
this.$emit('reload')

0 commit comments

Comments
 (0)