mirror of
https://github.com/nextcloud/server.git
synced 2026-10-16 03:51:45 +02:00
`occ share:list` resolved the source node of every share without handling NotFoundException, so a single orphaned share (e.g. the source file was deleted from disk externally, or is pending removal by DeleteOrphanedSharesJob) aborted the whole command with "Node for share not found" and produced no output at all. Catch the exception in both places that resolve the share node: orphaned shares are now listed with a null source-path, and the --parent filter skips them instead of crashing. Add unit tests for the command (previously untested), covering listing with an orphaned share present and the --parent filter with and without --recursive. Closes #61949 Assisted-by: ClaudeCode:claude-fable-5 Signed-off-by: Fabio Fantoni <fabio.fantoni@m2r.biz>
175 lines
5.5 KiB
PHP
175 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Files_Sharing\Command;
|
|
|
|
use OC\Core\Command\Base;
|
|
use OCP\Files\Folder;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\Files\Node;
|
|
use OCP\Files\NotFoundException;
|
|
use OCP\Share\IManager;
|
|
use OCP\Share\IShare;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
|
|
class ListShares extends Base {
|
|
/** @var array<string, Node> */
|
|
private array $fileCache = [];
|
|
|
|
private const array SHARE_TYPE_NAMES = [
|
|
IShare::TYPE_USER => 'user',
|
|
IShare::TYPE_GROUP => 'group',
|
|
IShare::TYPE_LINK => 'link',
|
|
IShare::TYPE_EMAIL => 'email',
|
|
IShare::TYPE_REMOTE => 'remote',
|
|
IShare::TYPE_REMOTE_GROUP => 'group',
|
|
IShare::TYPE_ROOM => 'room',
|
|
IShare::TYPE_DECK => 'deck',
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly IManager $shareManager,
|
|
private readonly IRootFolder $rootFolder,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
#[\Override]
|
|
protected function configure() {
|
|
parent::configure();
|
|
$this
|
|
->setName('share:list')
|
|
->setDescription('List available shares')
|
|
->addOption('owner', null, InputOption::VALUE_REQUIRED, 'only show shares owned by a specific user')
|
|
->addOption('recipient', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific recipient')
|
|
->addOption('by', null, InputOption::VALUE_REQUIRED, 'only show shares with by as specific user')
|
|
->addOption('file', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific file')
|
|
->addOption('parent', null, InputOption::VALUE_REQUIRED, 'only show shares of files inside a specific folder')
|
|
->addOption('recursive', null, InputOption::VALUE_NONE, 'also show shares nested deep inside the specified parent folder')
|
|
->addOption('type', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific type')
|
|
->addOption('status', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific status');
|
|
}
|
|
|
|
#[\Override]
|
|
public function execute(InputInterface $input, OutputInterface $output): int {
|
|
if ($input->getOption('recursive') && !$input->getOption('parent')) {
|
|
$output->writeln("<error>recursive option can't be used without parent option</error>");
|
|
return 1;
|
|
}
|
|
|
|
// todo: do some pre-filtering instead of first querying all shares
|
|
/** @var \Iterator<IShare> $allShares */
|
|
$allShares = $this->shareManager->getAllShares();
|
|
$shares = new \CallbackFilterIterator($allShares, function (IShare $share) use ($input) {
|
|
return $this->shouldShowShare($input, $share);
|
|
});
|
|
$shares = iterator_to_array($shares);
|
|
$data = array_map(function (IShare $share) {
|
|
try {
|
|
$sourcePath = $share->getNode()->getPath();
|
|
} catch (NotFoundException) {
|
|
// orphaned share, source file no longer exists
|
|
$sourcePath = null;
|
|
}
|
|
return [
|
|
'id' => $share->getId(),
|
|
'file' => $share->getNodeId(),
|
|
'target-path' => $share->getTarget(),
|
|
'source-path' => $sourcePath,
|
|
'owner' => $share->getShareOwner(),
|
|
'recipient' => $share->getSharedWith(),
|
|
'by' => $share->getSharedBy(),
|
|
'type' => self::SHARE_TYPE_NAMES[$share->getShareType()] ?? 'unknown',
|
|
];
|
|
}, $shares);
|
|
|
|
$this->writeTableInOutputFormat($input, $output, $data);
|
|
return 0;
|
|
}
|
|
|
|
private function getFileId(string $file): int {
|
|
if (is_numeric($file)) {
|
|
return (int)$file;
|
|
}
|
|
return $this->getFile($file)->getId();
|
|
}
|
|
|
|
private function getFile(string $file): Node {
|
|
if (isset($this->fileCache[$file])) {
|
|
return $this->fileCache[$file];
|
|
}
|
|
|
|
if (is_numeric($file)) {
|
|
$node = $this->rootFolder->getFirstNodeById((int)$file);
|
|
if (!$node) {
|
|
throw new NotFoundException("File with id $file not found");
|
|
}
|
|
} else {
|
|
$node = $this->rootFolder->get($file);
|
|
}
|
|
$this->fileCache[$file] = $node;
|
|
return $node;
|
|
}
|
|
|
|
private function getShareType(string $type): int {
|
|
foreach (self::SHARE_TYPE_NAMES as $shareType => $shareTypeName) {
|
|
if ($shareTypeName === $type) {
|
|
return $shareType;
|
|
}
|
|
}
|
|
throw new \Exception("Unknown share type $type");
|
|
}
|
|
|
|
private function shouldShowShare(InputInterface $input, IShare $share): bool {
|
|
if ($input->getOption('owner') && $share->getShareOwner() !== $input->getOption('owner')) {
|
|
return false;
|
|
}
|
|
if ($input->getOption('recipient') && $share->getSharedWith() !== $input->getOption('recipient')) {
|
|
return false;
|
|
}
|
|
if ($input->getOption('by') && $share->getSharedBy() !== $input->getOption('by')) {
|
|
return false;
|
|
}
|
|
if ($input->getOption('file') && $share->getNodeId() !== $this->getFileId($input->getOption('file'))) {
|
|
return false;
|
|
}
|
|
if ($input->getOption('parent')) {
|
|
$parent = $this->getFile($input->getOption('parent'));
|
|
if (!$parent instanceof Folder) {
|
|
throw new \Exception("Parent {$parent->getPath()} is not a folder");
|
|
}
|
|
$recursive = $input->getOption('recursive');
|
|
try {
|
|
if (!$recursive) {
|
|
$shareCacheEntry = $share->getNodeCacheEntry();
|
|
if (!$shareCacheEntry) {
|
|
$shareCacheEntry = $share->getNode();
|
|
}
|
|
if ($shareCacheEntry->getParentId() !== $parent->getId()) {
|
|
return false;
|
|
}
|
|
} else {
|
|
$shareNode = $share->getNode();
|
|
if ($parent->getRelativePath($shareNode->getPath()) === null) {
|
|
return false;
|
|
}
|
|
}
|
|
} catch (NotFoundException) {
|
|
// orphaned share, can't be inside the parent folder
|
|
return false;
|
|
}
|
|
}
|
|
if ($input->getOption('type') && $share->getShareType() !== $this->getShareType($input->getOption('type'))) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|