Skip to content

Commit

Permalink
Merge pull request #46596 from nextcloud/feat/folder-tree
Browse files Browse the repository at this point in the history
feat: Navigate via folder tree
  • Loading branch information
blizzz authored Aug 1, 2024
2 parents 4488714 + 7f6d6d9 commit ef7d830
Show file tree
Hide file tree
Showing 40 changed files with 763 additions and 160 deletions.
5 changes: 5 additions & 0 deletions apps/files/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
'url' => '/api/v1/views/{view}/{key}',
'verb' => 'PUT'
],
[
'name' => 'Api#setViewConfig',
'url' => '/api/v1/views',
'verb' => 'PUT'
],
[
'name' => 'Api#getViewConfigs',
'url' => '/api/v1/views',
Expand Down
6 changes: 6 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IServerContainer;
Expand All @@ -53,6 +55,7 @@
use OCP\Share\IManager as IShareManager;
use OCP\Util;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

class Application extends App implements IBootstrap {
public const APP_ID = 'files';
Expand Down Expand Up @@ -80,6 +83,9 @@ public function register(IRegistrationContext $context): void {
$server->getUserFolder(),
$c->get(UserConfig::class),
$c->get(ViewConfig::class),
$c->get(IL10N::class),
$c->get(IRootFolder::class),
$c->get(LoggerInterface::class),
);
});

Expand Down
122 changes: 97 additions & 25 deletions apps/files/lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
*/
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;
use OCA\Files\Service\ViewConfig;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
Expand All @@ -24,48 +29,44 @@
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;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
use Throwable;

/**
* @psalm-import-type FilesFolderTree from ResponseDefinitions
*
* @package OCA\Files\Controller
*/
class ApiController extends Controller {
private TagService $tagService;
private IManager $shareManager;
private IPreview $previewManager;
private IUserSession $userSession;
private IConfig $config;
private ?Folder $userFolder;
private UserConfig $userConfig;
private ViewConfig $viewConfig;

public function __construct(string $appName,
IRequest $request,
IUserSession $userSession,
TagService $tagService,
IPreview $previewManager,
IManager $shareManager,
IConfig $config,
?Folder $userFolder,
UserConfig $userConfig,
ViewConfig $viewConfig) {
private IUserSession $userSession,
private TagService $tagService,
private IPreview $previewManager,
private IManager $shareManager,
private IConfig $config,
private ?Folder $userFolder,
private UserConfig $userConfig,
private ViewConfig $viewConfig,
private IL10N $l10n,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
$this->userSession = $userSession;
$this->tagService = $tagService;
$this->previewManager = $previewManager;
$this->shareManager = $shareManager;
$this->config = $config;
$this->userFolder = $userFolder;
$this->userConfig = $userConfig;
$this->viewConfig = $viewConfig;
}

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

/**
* @param Folder[] $folders
*/
private function getTree(array $folders): array {
$user = $this->userSession->getUser();
if (!($user instanceof IUser)) {
throw new NotLoggedInException();
}

$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$tree = [];
foreach ($folders as $folder) {
$path = $userFolder->getRelativePath($folder->getPath());
if ($path === null) {
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];
}
}
return $tree['children'] ?? $tree;
}

/**
* Returns the folder tree of the user
*
* @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
*
* 200: Folder tree returned successfully
* 401: Unauthorized
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')]
public function getFolderTree(): 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);
} catch (Throwable $th) {
$this->logger->error($th->getMessage(), ['exception' => $th]);
$tree = [];
}
return new JSONResponse($tree, Http::STATUS_OK, [], JSON_FORCE_OBJECT);
}

/**
* Returns the current logged-in user's storage stats.
Expand Down
9 changes: 9 additions & 0 deletions apps/files/lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@
* content: string,
* type: string,
* }
*
* @psalm-type FilesFolderTreeNode = array{
* id: int,
* displayName?: string,
* children?: array<string, array{}>,
* }
*
* @psalm-type FilesFolderTree = array<string, FilesFolderTreeNode>
*
*/
class ResponseDefinitions {
}
8 changes: 7 additions & 1 deletion apps/files/lib/Service/UserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ class UserConfig {
'default' => false,
'allowed' => [true, false],
],
[
// Whether to show the folder tree
'key' => 'folder_tree',
'default' => true,
'allowed' => [true, false],
],
];

protected IConfig $config;
Expand Down Expand Up @@ -108,7 +114,7 @@ public function setConfig(string $key, $value): void {
if (!in_array($key, $this->getAllowedConfigKeys())) {
throw new \InvalidArgumentException('Unknown config key');
}

if (!in_array($value, $this->getAllowedConfigValues($key))) {
throw new \InvalidArgumentException('Invalid config value');
}
Expand Down
86 changes: 86 additions & 0 deletions apps/files/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,33 @@
}
}
},
"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"
}
}
}
},
"OCSMeta": {
"type": "object",
"required": [
Expand Down Expand Up @@ -1928,6 +1955,65 @@
}
}
}
},
"/ocs/v2.php/apps/files/api/v1/folder-tree": {
"get": {
"operationId": "api-get-folder-tree",
"summary": "Returns the folder tree of the user",
"tags": [
"api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Folder tree returned successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FolderTree"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"tags": []
Expand Down
24 changes: 22 additions & 2 deletions apps/files/src/components/BreadCrumbs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,11 @@ export default defineComponent({
return this.dirs.map((dir: string, index: number) => {
const source = this.getFileSourceFromPath(dir)
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
const to = { ...this.$route, params: { node: node?.fileid }, query: { dir } }
return {
dir,
exact: true,
name: this.getDirDisplayName(dir),
to,
to: this.getTo(dir, node),
// disable drop on current directory
disableDrop: index === this.dirs.length - 1,
}
Expand Down Expand Up @@ -163,6 +162,27 @@ export default defineComponent({
return node?.displayname || basename(path)
},
getTo(dir: string, node?: Node): Record<string, unknown> {
if (dir === '/') {
return {
...this.$route,
params: { view: this.currentView?.id },
query: {},
}
}
if (node === undefined) {
return {
...this.$route,
query: { dir },
}
}
return {
...this.$route,
params: { fileid: String(node.fileid) },
query: { dir: node.path },
}
},
onClick(to) {
if (to?.query?.dir === this.$route.query.dir) {
this.$emit('reload')
Expand Down
Loading

0 comments on commit ef7d830

Please sign in to comment.