Skip to content

Commit ccaa463

Browse files
authored
Merge pull request #51399 from nextcloud/share-list-cmd
add command to list shares
2 parents 5b98b6e + b9723ea commit ccaa463

File tree

7 files changed

+177
-0
lines changed

7 files changed

+177
-0
lines changed

apps/federatedfilesharing/lib/FederatedShareProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,7 @@ private function createShareObject($data) {
812812
->setPermissions((int)$data['permissions'])
813813
->setTarget($data['file_target'])
814814
->setMailSend((bool)$data['mail_send'])
815+
->setStatus((int)$data['accepted'])
815816
->setToken($data['token']);
816817

817818
$shareTime = new \DateTime();

apps/files_sharing/appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Turning the feature off removes shared files and folders on the server for all s
5050
<command>OCA\Files_Sharing\Command\ExiprationNotification</command>
5151
<command>OCA\Files_Sharing\Command\DeleteOrphanShares</command>
5252
<command>OCA\Files_Sharing\Command\FixShareOwners</command>
53+
<command>OCA\Files_Sharing\Command\ListShares</command>
5354
</commands>
5455

5556
<settings>

apps/files_sharing/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => $baseDir . '/../lib/Command/DeleteOrphanShares.php',
2929
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => $baseDir . '/../lib/Command/ExiprationNotification.php',
3030
'OCA\\Files_Sharing\\Command\\FixShareOwners' => $baseDir . '/../lib/Command/FixShareOwners.php',
31+
'OCA\\Files_Sharing\\Command\\ListShares' => $baseDir . '/../lib/Command/ListShares.php',
3132
'OCA\\Files_Sharing\\Controller\\AcceptController' => $baseDir . '/../lib/Controller/AcceptController.php',
3233
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => $baseDir . '/../lib/Controller/DeletedShareAPIController.php',
3334
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => $baseDir . '/../lib/Controller/ExternalSharesController.php',

apps/files_sharing/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class ComposerStaticInitFiles_Sharing
4343
'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => __DIR__ . '/..' . '/../lib/Command/DeleteOrphanShares.php',
4444
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => __DIR__ . '/..' . '/../lib/Command/ExiprationNotification.php',
4545
'OCA\\Files_Sharing\\Command\\FixShareOwners' => __DIR__ . '/..' . '/../lib/Command/FixShareOwners.php',
46+
'OCA\\Files_Sharing\\Command\\ListShares' => __DIR__ . '/..' . '/../lib/Command/ListShares.php',
4647
'OCA\\Files_Sharing\\Controller\\AcceptController' => __DIR__ . '/..' . '/../lib/Controller/AcceptController.php',
4748
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/DeletedShareAPIController.php',
4849
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => __DIR__ . '/..' . '/../lib/Controller/ExternalSharesController.php',
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Robin Appelman <[email protected]>
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Files_Sharing\Command;
10+
11+
use OC\Core\Command\Base;
12+
use OCP\Files\Folder;
13+
use OCP\Files\IRootFolder;
14+
use OCP\Files\Node;
15+
use OCP\Files\NotFoundException;
16+
use OCP\Share\IManager;
17+
use OCP\Share\IShare;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
22+
class ListShares extends Base {
23+
/** @var array<string, Node> */
24+
private array $fileCache = [];
25+
26+
private const SHARE_TYPE_NAMES = [
27+
IShare::TYPE_USER => 'user',
28+
IShare::TYPE_GROUP => 'group',
29+
IShare::TYPE_LINK => 'link',
30+
IShare::TYPE_EMAIL => 'email',
31+
IShare::TYPE_REMOTE => 'remote',
32+
IShare::TYPE_REMOTE_GROUP => 'group',
33+
IShare::TYPE_ROOM => 'room',
34+
IShare::TYPE_DECK => 'deck',
35+
];
36+
37+
public function __construct(
38+
private readonly IManager $shareManager,
39+
private readonly IRootFolder $rootFolder,
40+
) {
41+
parent::__construct();
42+
}
43+
44+
protected function configure() {
45+
parent::configure();
46+
$this
47+
->setName('share:list')
48+
->setDescription('List available shares')
49+
->addOption('owner', null, InputOption::VALUE_REQUIRED, 'only show shares owned by a specific user')
50+
->addOption('recipient', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific recipient')
51+
->addOption('by', null, InputOption::VALUE_REQUIRED, 'only show shares with by as specific user')
52+
->addOption('file', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific file')
53+
->addOption('parent', null, InputOption::VALUE_REQUIRED, 'only show shares of files inside a specific folder')
54+
->addOption('recursive', null, InputOption::VALUE_NONE, 'also show shares nested deep inside the specified parent folder')
55+
->addOption('type', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific type')
56+
->addOption('status', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific status');
57+
}
58+
59+
public function execute(InputInterface $input, OutputInterface $output): int {
60+
if ($input->getOption('recursive') && !$input->getOption('parent')) {
61+
$output->writeln("<error>recursive option can't be used without parent option</error>");
62+
return 1;
63+
}
64+
65+
// todo: do some pre-filtering instead of first querying all shares
66+
/** @var \Iterator<IShare> $allShares */
67+
$allShares = $this->shareManager->getAllShares();
68+
$shares = new \CallbackFilterIterator($allShares, function (IShare $share) use ($input) {
69+
return $this->shouldShowShare($input, $share);
70+
});
71+
$shares = iterator_to_array($shares);
72+
$data = array_map(function (IShare $share) {
73+
return [
74+
'id' => $share->getId(),
75+
'file' => $share->getNodeId(),
76+
'target-path' => $share->getTarget(),
77+
'source-path' => $share->getNode()->getPath(),
78+
'owner' => $share->getShareOwner(),
79+
'recipient' => $share->getSharedWith(),
80+
'by' => $share->getSharedBy(),
81+
'type' => self::SHARE_TYPE_NAMES[$share->getShareType()] ?? 'unknown',
82+
];
83+
}, $shares);
84+
85+
$this->writeTableInOutputFormat($input, $output, $data);
86+
return 0;
87+
}
88+
89+
private function getFileId(string $file): int {
90+
if (is_numeric($file)) {
91+
return (int)$file;
92+
}
93+
return $this->getFile($file)->getId();
94+
}
95+
96+
private function getFile(string $file): Node {
97+
if (isset($this->fileCache[$file])) {
98+
return $this->fileCache[$file];
99+
}
100+
101+
if (is_numeric($file)) {
102+
$node = $this->rootFolder->getFirstNodeById((int)$file);
103+
if (!$node) {
104+
throw new NotFoundException("File with id $file not found");
105+
}
106+
} else {
107+
$node = $this->rootFolder->get($file);
108+
}
109+
$this->fileCache[$file] = $node;
110+
return $node;
111+
}
112+
113+
private function getShareType(string $type): int {
114+
foreach (self::SHARE_TYPE_NAMES as $shareType => $shareTypeName) {
115+
if ($shareTypeName === $type) {
116+
return $shareType;
117+
}
118+
}
119+
throw new \Exception("Unknown share type $type");
120+
}
121+
122+
private function shouldShowShare(InputInterface $input, IShare $share): bool {
123+
if ($input->getOption('owner') && $share->getShareOwner() !== $input->getOption('owner')) {
124+
return false;
125+
}
126+
if ($input->getOption('recipient') && $share->getSharedWith() !== $input->getOption('recipient')) {
127+
return false;
128+
}
129+
if ($input->getOption('by') && $share->getSharedBy() !== $input->getOption('by')) {
130+
return false;
131+
}
132+
if ($input->getOption('file') && $share->getNodeId() !== $this->getFileId($input->getOption('file'))) {
133+
return false;
134+
}
135+
if ($input->getOption('parent')) {
136+
$parent = $this->getFile($input->getOption('parent'));
137+
if (!$parent instanceof Folder) {
138+
throw new \Exception("Parent {$parent->getPath()} is not a folder");
139+
}
140+
$recursive = $input->getOption('recursive');
141+
if (!$recursive) {
142+
$shareCacheEntry = $share->getNodeCacheEntry();
143+
if (!$shareCacheEntry) {
144+
$shareCacheEntry = $share->getNode();
145+
}
146+
if ($shareCacheEntry->getParentId() !== $parent->getId()) {
147+
return false;
148+
}
149+
} else {
150+
$shareNode = $share->getNode();
151+
if ($parent->getRelativePath($shareNode->getPath()) === null) {
152+
return false;
153+
}
154+
}
155+
}
156+
if ($input->getOption('type') && $share->getShareType() !== $this->getShareType($input->getOption('type'))) {
157+
return false;
158+
}
159+
return true;
160+
}
161+
}

lib/private/Files/Cache/CacheEntry.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ public function getUploadTime(): ?int {
110110
return $this->data['upload_time'] ?? null;
111111
}
112112

113+
public function getParentId(): int {
114+
return $this->data['parent'];
115+
}
116+
113117
public function getData() {
114118
return $this->data;
115119
}

lib/public/Files/Cache/ICacheEntry.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,12 @@ public function getUploadTime(): ?int;
161161
* @since 25.0.0
162162
*/
163163
public function getUnencryptedSize(): int;
164+
165+
/**
166+
* Get the file id of the parent folder
167+
*
168+
* @return int
169+
* @since 32.0.0
170+
*/
171+
public function getParentId(): int;
164172
}

0 commit comments

Comments
 (0)