diff --git a/apps/files/lib/Command/Copy.php b/apps/files/lib/Command/Copy.php index 1d905e73e986b..54d2be72f74da 100644 --- a/apps/files/lib/Command/Copy.php +++ b/apps/files/lib/Command/Copy.php @@ -9,77 +9,77 @@ namespace OCA\Files\Command; use OC\Core\Command\Info\FileUtils; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\IQuestionHelper; use OCP\Files\Folder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; -class Copy extends Command { +#[AsCommand( + name: 'files:copy', + description: 'Copy a file or folder', +)] +class Copy { public function __construct( - private FileUtils $fileUtils, + private readonly FileUtils $fileUtils, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:copy') - ->setDescription('Copy a file or folder') - ->addArgument('source', InputArgument::REQUIRED, 'Source file id or path') - ->addArgument('target', InputArgument::REQUIRED, 'Target path') - ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for confirmation and don't output any warnings") - ->addOption('no-target-directory', 'T', InputOption::VALUE_NONE, 'When target path is folder, overwrite the folder instead of copying into the folder'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $sourceInput = $input->getArgument('source'); - $targetInput = $input->getArgument('target'); - $force = $input->getOption('force'); - $noTargetDir = $input->getOption('no-target-directory'); - - $node = $this->fileUtils->getNode($sourceInput); - $targetNode = $this->fileUtils->getNode($targetInput); + public function __invoke( + IOutput $output, + IQuestionHelper $questionHelper, + #[Argument(description: 'Source file id or path')] string $source, + #[Argument(description: 'Target path')] string $target, + #[Option( + description: "Don't ask for confirmation and don't output any warnings", + shortcut: 'f', + )] bool $force = false, + #[Option( + name: 'no-target-directory', + description: 'When target path is folder, overwrite the folder instead of copying into the folder', + shortcut: 'T', + )] bool $noTargetDirectory = false, + ): ExitCode { + $node = $this->fileUtils->getNode($source); + $targetNode = $this->fileUtils->getNode($target); if (!$node) { - $output->writeln("file $sourceInput not found"); - return 1; + $output->writeln("file $source not found"); + return ExitCode::Failure; } - $targetParentPath = dirname(rtrim($targetInput, '/')); + $targetParentPath = dirname(rtrim($target, '/')); $targetParent = $this->fileUtils->getNode($targetParentPath); if (!$targetParent) { $output->writeln("Target parent path $targetParentPath doesn't exist"); - return 1; + return ExitCode::Failure; } $wouldRequireDelete = false; if ($targetNode) { if (!$targetNode->isUpdateable()) { - $output->writeln("$targetInput isn't writable"); - return 1; + $output->writeln("$target isn't writable"); + return ExitCode::Failure; } if ($targetNode instanceof Folder) { - if ($noTargetDir) { + if ($noTargetDirectory) { if (!$force) { - $output->writeln("Warning: $sourceInput is a file, but $targetInput is a folder"); + $output->writeln("Warning: $source is a file, but $target is a folder"); } $wouldRequireDelete = true; } else { - $targetInput = $targetNode->getFullPath($node->getName()); - $targetNode = $this->fileUtils->getNode($targetInput); + $target = $targetNode->getFullPath($node->getName()); + $targetNode = $this->fileUtils->getNode($target); } } else { if ($node instanceof Folder) { if (!$force) { - $output->writeln("Warning: $sourceInput is a folder, but $targetInput is a file"); + $output->writeln("Warning: $source is a folder, but $target is a file"); } $wouldRequireDelete = true; } @@ -87,21 +87,18 @@ public function execute(InputInterface $input, OutputInterface $output): int { if ($wouldRequireDelete && $targetNode->getInternalPath() === '') { $output->writeln("Mount root can't be overwritten with a different type"); - return 1; + return ExitCode::Failure; } if ($wouldRequireDelete && !$targetNode->isDeletable()) { - $output->writeln("$targetInput can't be deleted to be replaced with $sourceInput"); - return 1; + $output->writeln("$target can't be deleted to be replaced with $source"); + return ExitCode::Failure; } if (!$force && $targetNode) { - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); - - $question = new ConfirmationQuestion('' . $targetInput . ' already exists, overwrite? [y/N] ', false); - if (!$helper->ask($input, $output, $question)) { - return 1; + $question = new ConfirmationQuestion('' . $target . ' already exists, overwrite? [y/N] ', false); + if (!$questionHelper->ask($question)) { + return ExitCode::Failure; } } } @@ -110,9 +107,8 @@ public function execute(InputInterface $input, OutputInterface $output): int { $targetNode->delete(); } - $node->copy($targetInput); + $node->copy($target); - return 0; + return ExitCode::Success; } - } diff --git a/apps/files/lib/Command/Delete.php b/apps/files/lib/Command/Delete.php index 627b993bcce5d..17ac1f09c121c 100644 --- a/apps/files/lib/Command/Delete.php +++ b/apps/files/lib/Command/Delete.php @@ -11,58 +11,54 @@ use OC\Core\Command\Info\FileUtils; use OCA\Files_Sharing\SharedStorage; use OCA\Files_Trashbin\Trash\ITrashManager; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\IQuestionHelper; use OCP\Files\Folder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; -class Delete extends Command { +#[AsCommand( + name: 'files:delete', + description: 'Delete a file or folder', +)] +class Delete { public function __construct( private readonly FileUtils $fileUtils, private readonly ?ITrashManager $trashManager = null, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:delete') - ->setDescription('Delete a file or folder') - ->addArgument('file', InputArgument::REQUIRED, 'File id or path') - ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings") - ->addOption('skip-trash', null, InputOption::VALUE_NONE, 'Bypass the trashbin when deleting the file or folder'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $fileInput = $input->getArgument('file'); - $inputIsId = is_numeric($fileInput); - $force = $input->getOption('force'); - $skipTrash = $input->getOption('skip-trash'); - $node = $this->fileUtils->getNode($fileInput); + public function __invoke( + IOutput $output, + IQuestionHelper $questionHelper, + #[Argument(description: 'File id or path')] string $file, + #[Option( + description: "Don't ask for configuration and don't output any warnings", + shortcut: 'f', + )] bool $force = false, + #[Option(name: 'skip-trash', description: 'Bypass the trashbin when deleting the file or folder')] bool $skipTrash = false, + ): ExitCode { + $inputIsId = is_numeric($file); + $node = $this->fileUtils->getNode($file); if (!$node) { - $output->writeln("file $fileInput not found"); - return self::FAILURE; + $output->writeln("file $file not found"); + return ExitCode::Failure; } $deleteConfirmed = $force; if (!$deleteConfirmed) { - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); $storage = $node->getStorage(); if (!$inputIsId && $storage->instanceOfStorage(SharedStorage::class) && $node->getInternalPath() === '') { /** @var SharedStorage $storage */ - [,$user] = explode('/', $fileInput, 3); - $question = new ConfirmationQuestion("$fileInput in a shared file, do you want to unshare the file from $user instead of deleting the source file? [Y/n] ", true); - if ($helper->ask($input, $output, $question)) { + [,$user] = explode('/', $file, 3); + $question = new ConfirmationQuestion("$file in a shared file, do you want to unshare the file from $user instead of deleting the source file? [Y/n] ", true); + if ($questionHelper->ask($question)) { $storage->unshareStorage(); - return self::SUCCESS; + return ExitCode::Success; } else { $node = $storage->getShare()->getNode(); $output->writeln(''); @@ -76,8 +72,8 @@ public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln(''); foreach ($filesByUsers as $user => $filesByUser) { $output->writeln($user . ':'); - foreach ($filesByUser as $file) { - $output->writeln(' - ' . $file->getPath()); + foreach ($filesByUser as $userFile) { + $output->writeln(' - ' . $userFile->getPath()); } } $output->writeln(''); @@ -89,7 +85,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { $maybeContents = ''; } $question = new ConfirmationQuestion('Delete ' . $node->getPath() . $maybeContents . '? [y/N] ', false); - $deleteConfirmed = $helper->ask($input, $output, $question); + $deleteConfirmed = $questionHelper->ask($question); } if ($deleteConfirmed) { @@ -104,6 +100,6 @@ public function execute(InputInterface $input, OutputInterface $output): int { } } - return self::SUCCESS; + return ExitCode::Success; } } diff --git a/apps/files/lib/Command/DeleteOrphanedFiles.php b/apps/files/lib/Command/DeleteOrphanedFiles.php index c5a15c304a6fd..1ca4ba722f33b 100644 --- a/apps/files/lib/Command/DeleteOrphanedFiles.php +++ b/apps/files/lib/Command/DeleteOrphanedFiles.php @@ -1,5 +1,7 @@ setName('files:cleanup') - ->setDescription('Clean up orphaned filecache and mount entries') - ->setHelp('Deletes orphaned filecache and mount entries (those without an existing storage).') - ->addOption('skip-filecache-extended', null, InputOption::VALUE_NONE, 'don\'t remove orphaned entries from filecache_extended'); } - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { + public function __invoke( + IOutput $output, + #[Option(name: 'skip-filecache-extended', description: 'don\'t remove orphaned entries from filecache_extended')] + bool $skipFilecacheExtended = false, + ): ExitCode { $fileIdsByStorage = []; $deletedStorages = array_diff($this->getReferencedStorages(), $this->getExistingStorages()); - $deleteExtended = !$input->getOption('skip-filecache-extended'); + $deleteExtended = !$skipFilecacheExtended; if ($deleteExtended) { $fileIdsByStorage = $this->getFileIdsForStorages($deletedStorages); } @@ -58,7 +58,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { $deletedMounts = $this->cleanupOrphanedMounts(); $output->writeln("$deletedMounts orphaned mount entries deleted"); - return self::SUCCESS; + return ExitCode::Success; } private function getReferencedStorages(): array { diff --git a/apps/files/lib/Command/Get.php b/apps/files/lib/Command/Get.php index 81b9c308385fc..13ecd76bc64f9 100644 --- a/apps/files/lib/Command/Get.php +++ b/apps/files/lib/Command/Get.php @@ -9,65 +9,60 @@ namespace OCA\Files\Command; use OC\Core\Command\Info\FileUtils; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; use OCP\Files\File; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -class Get extends Command { +#[AsCommand( + name: 'files:get', + description: 'Get the contents of a file', +)] +class Get { public function __construct( - private FileUtils $fileUtils, + private readonly FileUtils $fileUtils, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:get') - ->setDescription('Get the contents of a file') - ->addArgument('file', InputArgument::REQUIRED, 'Source file id or Nextcloud path') - ->addArgument('output', InputArgument::OPTIONAL, 'Target local file to output to, defaults to STDOUT'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $fileInput = $input->getArgument('file'); - $outputName = $input->getArgument('output'); - $node = $this->fileUtils->getNode($fileInput); + public function __invoke( + IOutput $output, + #[Argument(description: 'Source file id or Nextcloud path')] string $file, + #[Argument(name: 'output', description: 'Target local file to output to, defaults to STDOUT')] ?string $outputFile = null, + ): ExitCode { + $node = $this->fileUtils->getNode($file); if (!$node) { - $output->writeln("file $fileInput not found"); - return self::FAILURE; + $output->writeln("file $file not found"); + return ExitCode::Failure; } if (!($node instanceof File)) { - $output->writeln("$fileInput is a directory"); - return self::FAILURE; + $output->writeln("$file is a directory"); + return ExitCode::Failure; } $isTTY = stream_isatty(STDOUT); - if ($outputName === null && $isTTY && $node->getMimePart() !== 'text') { + if ($outputFile === null && $isTTY && $node->getMimePart() !== 'text') { $output->writeln([ 'Warning: Binary output can mess up your terminal', - " Use occ files:get $fileInput - to output it to the terminal anyway", - " Or occ files:get $fileInput to save to a file instead" + " Use occ files:get $file - to output it to the terminal anyway", + " Or occ files:get $file to save to a file instead" ]); - return self::FAILURE; + return ExitCode::Failure; } $source = $node->fopen('r'); if (!$source) { - $output->writeln("Failed to open $fileInput for reading"); - return self::FAILURE; + $output->writeln("Failed to open $file for reading"); + return ExitCode::Failure; } - $target = ($outputName === null || $outputName === '-') ? STDOUT : fopen($outputName, 'w'); + $target = ($outputFile === null || $outputFile === '-') ? STDOUT : fopen($outputFile, 'w'); if (!$target) { - $output->writeln("Failed to open $outputName for reading"); - return self::FAILURE; + $output->writeln("Failed to open $outputFile for reading"); + return ExitCode::Failure; } stream_copy_to_stream($source, $target); - return self::SUCCESS; + return ExitCode::Success; } } diff --git a/apps/files/lib/Command/Mkdir.php b/apps/files/lib/Command/Mkdir.php index a7c47ccd362b7..a937bc839fdf0 100644 --- a/apps/files/lib/Command/Mkdir.php +++ b/apps/files/lib/Command/Mkdir.php @@ -9,46 +9,42 @@ namespace OCA\Files\Command; use OC\Core\Command\Info\FileUtils; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -class Mkdir extends Command { +#[AsCommand( + name: 'files:mkdir', + description: 'Create a new directory', +)] +class Mkdir { public function __construct( private readonly FileUtils $fileUtils, private readonly IRootFolder $rootFolder, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:mkdir') - ->setDescription('Create a new directory') - ->addArgument('path', InputArgument::REQUIRED, 'Target Nextcloud path for the new folder'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $path = $input->getArgument('path'); + public function __invoke( + IOutput $output, + #[Argument(description: 'Target Nextcloud path for the new folder')] string $path, + ): ExitCode { $node = $this->fileUtils->getNode($path); if ($node instanceof Folder) { $output->writeln("$path already exists"); - return self::SUCCESS; + return ExitCode::Success; } if ($node instanceof File) { $output->writeln("$path is a file"); - return self::FAILURE; + return ExitCode::Failure; } $this->rootFolder->newFolder($path); - return self::SUCCESS; + return ExitCode::Success; } } diff --git a/apps/files/lib/Command/Mount/ListMounts.php b/apps/files/lib/Command/Mount/ListMounts.php index ae72d236c97fb..91407fc53e653 100644 --- a/apps/files/lib/Command/Mount/ListMounts.php +++ b/apps/files/lib/Command/Mount/ListMounts.php @@ -8,44 +8,41 @@ namespace OCA\Files\Command\Mount; -use OC\Core\Command\Base; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IInput; +use OCP\Console\IOutput; use OCP\Files\Config\ICachedMountInfo; use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Config\IUserMountCache; use OCP\Files\Mount\IMountPoint; use OCP\IUserManager; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class ListMounts extends Base { +#[AsCommand( + name: 'files:mount:list', + description: 'List of mounts for a user', +)] +class ListMounts { public function __construct( private readonly IUserManager $userManager, private readonly IUserMountCache $userMountCache, private readonly IMountProviderCollection $mountProviderCollection, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - parent::configure(); - $this - ->setName('files:mount:list') - ->setDescription('List of mounts for a user') - ->addArgument('user', InputArgument::REQUIRED, 'User to list mounts for') - ->addOption('cached-only', null, InputOption::VALUE_NONE, 'Only return cached mounts, prevents filesystem setup'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $userId = $input->getArgument('user'); - $cachedOnly = $input->getOption('cached-only'); + public function __invoke( + IInput $input, + IOutput $output, + #[Argument(description: 'User to list mounts for')] string $user, + #[Option(name: 'cached-only', description: 'Only return cached mounts, prevents filesystem setup')] bool $cachedOnly = false, + ): ExitCode { + $userId = $user; $user = $this->userManager->get($userId); if (!$user) { $output->writeln("User $userId not found"); - return 1; + return ExitCode::Failure; } if ($cachedOnly) { @@ -65,7 +62,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { $format = $input->getOption('output'); - if ($format === self::OUTPUT_FORMAT_PLAIN) { + if ($format === 'plain') { foreach ($mounts as $mount) { $output->writeln('' . $mount->getMountPoint() . ': ' . $mount->getStorageId()); if (isset($cachedByMountPoint[$mount->getMountPoint()])) { @@ -101,12 +98,11 @@ public function execute(InputInterface $input, OutputInterface $output): int { 'storage_id' => $cachedMountInfo->getStorageId(), 'root_id' => $cachedMountInfo->getStorageRootId(), ], $mounts); - $this->writeArrayInOutputFormat($input, $output, array_filter([ + $output->writeArrayInOutputFormat(array_filter([ 'cached' => $cached, 'provided' => $cachedOnly ? null : $provided, ])); } - return 0; + return ExitCode::Success; } - } diff --git a/apps/files/lib/Command/Mount/Refresh.php b/apps/files/lib/Command/Mount/Refresh.php index 3995345041a60..cf9a4c8b8fc06 100644 --- a/apps/files/lib/Command/Mount/Refresh.php +++ b/apps/files/lib/Command/Mount/Refresh.php @@ -8,38 +8,35 @@ namespace OCA\Files\Command\Mount; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Config\IUserMountCache; use OCP\IUserManager; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -class Refresh extends Command { +#[AsCommand( + name: 'files:mount:refresh', + description: 'Refresh the list of mounts for a user', +)] +class Refresh { public function __construct( private readonly IUserManager $userManager, private readonly IUserMountCache $userMountCache, private readonly IMountProviderCollection $mountProviderCollection, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:mount:refresh') - ->setDescription('Refresh the list of mounts for a user') - ->addArgument('user', InputArgument::REQUIRED, 'User to refresh mounts for'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $userId = $input->getArgument('user'); + public function __invoke( + IOutput $output, + #[Argument(description: 'User to refresh mounts for')] string $user, + ): ExitCode { + $userId = $user; $user = $this->userManager->get($userId); if (!$user) { $output->writeln("User $userId not found"); - return 1; + return ExitCode::Failure; } $mounts = $this->mountProviderCollection->getMountsForUser($user); @@ -49,7 +46,6 @@ public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Registered ' . count($mounts) . ' mounts'); - return 0; + return ExitCode::Success; } - } diff --git a/apps/files/lib/Command/Move.php b/apps/files/lib/Command/Move.php index ab4b9f86bfab8..82cb7f5ee15b7 100644 --- a/apps/files/lib/Command/Move.php +++ b/apps/files/lib/Command/Move.php @@ -9,89 +9,83 @@ namespace OCA\Files\Command; use OC\Core\Command\Info\FileUtils; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\IQuestionHelper; use OCP\Files\File; use OCP\Files\Folder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; -class Move extends Command { +#[AsCommand( + name: 'files:move', + description: 'Move a file or a folder', +)] +class Move { public function __construct( - private FileUtils $fileUtils, + private readonly FileUtils $fileUtils, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:move') - ->setDescription('Move a file or folder') - ->addArgument('source', InputArgument::REQUIRED, 'Source file id or path') - ->addArgument('target', InputArgument::REQUIRED, 'Target path') - ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings"); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $sourceInput = $input->getArgument('source'); - $targetInput = $input->getArgument('target'); - $force = $input->getOption('force'); - - $node = $this->fileUtils->getNode($sourceInput); - $targetNode = $this->fileUtils->getNode($targetInput); + public function __invoke( + IOutput $output, + IQuestionHelper $questionHelper, + #[Argument(description: 'Source file id or path')] string $source, + #[Argument(description: 'Target path')] string $target, + #[Option( + description: "Don't ask for configuration and don't output any warnings", + shortcut: 'f', + )] bool $force = false, + ): ExitCode { + $node = $this->fileUtils->getNode($source); + $targetNode = $this->fileUtils->getNode($target); if (!$node) { - $output->writeln("file $sourceInput not found"); - return 1; + $output->writeln("file $source not found"); + return ExitCode::Failure; } - $targetParentPath = dirname(rtrim($targetInput, '/')); + $targetParentPath = dirname(rtrim($target, '/')); $targetParent = $this->fileUtils->getNode($targetParentPath); if (!$targetParent) { $output->writeln("Target parent path $targetParentPath doesn't exist"); - return 1; + return ExitCode::Failure; } $wouldRequireDelete = false; if ($targetNode) { if (!$targetNode->isUpdateable()) { - $output->writeln("$targetInput already exists and isn't writable"); - return 1; + $output->writeln("$target already exists and isn't writable"); + return ExitCode::Failure; } if ($node instanceof Folder && $targetNode instanceof File) { - $output->writeln("Warning: $sourceInput is a folder, but $targetInput is a file"); + $output->writeln("Warning: $source is a folder, but $target is a file"); $wouldRequireDelete = true; } if ($node instanceof File && $targetNode instanceof Folder) { - $output->writeln("Warning: $sourceInput is a file, but $targetInput is a folder"); + $output->writeln("Warning: $source is a file, but $target is a folder"); $wouldRequireDelete = true; } if ($wouldRequireDelete && $targetNode->getInternalPath() === '') { $output->writeln("Mount root can't be overwritten with a different type"); - return 1; + return ExitCode::Failure; } if ($wouldRequireDelete && !$targetNode->isDeletable()) { - $output->writeln("$targetInput can't be deleted to be replaced with $sourceInput"); - return 1; + $output->writeln("$target can't be deleted to be replaced with $source"); + return ExitCode::Failure; } if (!$force) { - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); - - $question = new ConfirmationQuestion('' . $targetInput . ' already exists, overwrite? [y/N] ', false); - if (!$helper->ask($input, $output, $question)) { - return 1; + $question = new ConfirmationQuestion('' . $target . ' already exists, overwrite? [y/N] ', false); + if (!$questionHelper->ask($question)) { + return ExitCode::Failure; } } } @@ -100,9 +94,8 @@ public function execute(InputInterface $input, OutputInterface $output): int { $targetNode->delete(); } - $node->move($targetInput); + $node->move($target); - return 0; + return ExitCode::Success; } - } diff --git a/apps/files/lib/Command/Object/Delete.php b/apps/files/lib/Command/Object/Delete.php index 575bf5df069cb..ce3e7183de9cf 100644 --- a/apps/files/lib/Command/Object/Delete.php +++ b/apps/files/lib/Command/Object/Delete.php @@ -8,34 +8,31 @@ namespace OCA\Files\Command\Object; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\IQuestionHelper; use Symfony\Component\Console\Question\ConfirmationQuestion; -class Delete extends Command { +#[AsCommand( + name: 'files:object:delete', + description: 'Delete an object from the object store', +)] +class Delete { public function __construct( - private ObjectUtil $objectUtils, + private readonly ObjectUtil $objectUtils, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:object:delete') - ->setDescription('Delete an object from the object store') - ->addArgument('object', InputArgument::REQUIRED, 'Object to delete') - ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to delete the object from, only required in cases where it can't be determined from the config"); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $object = $input->getArgument('object'); - $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); + public function __invoke( + IOutput $output, + IQuestionHelper $questionHelper, + #[Argument(description: 'Object to delete')] string $object, + #[Option(description: "Bucket to delete the object from, only required in cases where it can't be determined from the config", shortcut: 'b')] ?string $bucket = null, + ): ExitCode|int { + $objectStore = $this->objectUtils->getObjectStore($bucket, $output); if (!$objectStore) { return -1; } @@ -51,12 +48,10 @@ public function execute(InputInterface $input, OutputInterface $output): int { return -1; } - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); $question = new ConfirmationQuestion("Delete $object? [y/N] ", false); - if ($helper->ask($input, $output, $question)) { + if ($questionHelper->ask($question)) { $objectStore->deleteObject($object); } - return self::SUCCESS; + return ExitCode::Success; } } diff --git a/apps/files/lib/Command/Object/Get.php b/apps/files/lib/Command/Object/Get.php index dc88ff6a94d3f..8cb9072ea2e9e 100644 --- a/apps/files/lib/Command/Object/Get.php +++ b/apps/files/lib/Command/Object/Get.php @@ -8,41 +8,36 @@ namespace OCA\Files\Command\Object; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -class Get extends Command { +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; + +#[AsCommand( + name: 'files:object:get', + description: 'Get the contents of an object', +)] +class Get { public function __construct( - private ObjectUtil $objectUtils, + private readonly ObjectUtil $objectUtils, ) { - parent::__construct(); - } - - #[\Override] - protected function configure(): void { - $this - ->setName('files:object:get') - ->setDescription('Get the contents of an object') - ->addArgument('object', InputArgument::REQUIRED, 'Object to get') - ->addArgument('output', InputArgument::REQUIRED, 'Target local file to output to, use - for STDOUT') - ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to get the object from, only required in cases where it can't be determined from the config"); } - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $object = $input->getArgument('object'); - $outputName = $input->getArgument('output'); - $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); + public function __invoke( + IOutput $output, + #[Argument(description: 'Object to get')] string $object, + #[Argument(name: 'output', description: 'Target local file to output to, use - for STDOUT')] string $outputFile, + #[Option(description: "Bucket to get the object from, only required in cases where it can't be determined from the config", shortcut: 'b')] ?string $bucket = null, + ): ExitCode { + $objectStore = $this->objectUtils->getObjectStore($bucket, $output); if (!$objectStore) { - return self::FAILURE; + return ExitCode::Failure; } if (!$objectStore->objectExists($object)) { $output->writeln("Object $object does not exist"); - return self::FAILURE; + return ExitCode::Failure; } try { @@ -50,16 +45,15 @@ public function execute(InputInterface $input, OutputInterface $output): int { } catch (\Exception $e) { $msg = $e->getMessage(); $output->writeln("Failed to read $object from object store: $msg"); - return self::FAILURE; + return ExitCode::Failure; } - $target = $outputName === '-' ? STDOUT : fopen($outputName, 'w'); + $target = $outputFile === '-' ? STDOUT : fopen($outputFile, 'w'); if (!$target) { - $output->writeln("Failed to open $outputName for writing"); - return self::FAILURE; + $output->writeln("Failed to open $outputFile for writing"); + return ExitCode::Failure; } stream_copy_to_stream($source, $target); - return self::SUCCESS; + return ExitCode::Success; } - } diff --git a/apps/files/lib/Command/Object/Info.php b/apps/files/lib/Command/Object/Info.php index 14b2ac1aed99f..9a06d1e8d70f7 100644 --- a/apps/files/lib/Command/Object/Info.php +++ b/apps/files/lib/Command/Object/Info.php @@ -8,49 +8,46 @@ namespace OCA\Files\Command\Object; -use OC\Core\Command\Base; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IInput; +use OCP\Console\IOutput; use OCP\Files\IMimeTypeDetector; use OCP\Files\ObjectStore\IObjectStoreMetaData; use OCP\Util; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class Info extends Base { +#[AsCommand( + name: 'files:object:info', + description: 'Get the metadata of an object', +)] +class Info { public function __construct( - private ObjectUtil $objectUtils, - private IMimeTypeDetector $mimeTypeDetector, + private readonly ObjectUtil $objectUtils, + private readonly IMimeTypeDetector $mimeTypeDetector, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - parent::configure(); - $this - ->setName('files:object:info') - ->setDescription('Get the metadata of an object') - ->addArgument('object', InputArgument::REQUIRED, 'Object to get') - ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to get the object from, only required in cases where it can't be determined from the config"); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $object = $input->getArgument('object'); - $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); + public function __invoke( + IInput $input, + IOutput $output, + #[Argument(description: 'Object to get')] string $object, + #[Option(description: "Bucket to get the object from, only required in cases where it can't be determined from the config", shortcut: 'b')] ?string $bucket = null, + ): ExitCode { + $objectStore = $this->objectUtils->getObjectStore($bucket, $output); if (!$objectStore) { - return self::FAILURE; + return ExitCode::Failure; } if (!$objectStore instanceof IObjectStoreMetaData) { $output->writeln('Configured object store does currently not support retrieve metadata'); - return self::FAILURE; + return ExitCode::Failure; } if (!$objectStore->objectExists($object)) { $output->writeln("Object $object does not exist"); - return self::FAILURE; + return ExitCode::Failure; } try { @@ -58,7 +55,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { } catch (\Exception $e) { $msg = $e->getMessage(); $output->writeln("Failed to read $object from object store: $msg"); - return self::FAILURE; + return ExitCode::Failure; } if ($input->getOption('output') === 'plain' && isset($meta['size'])) { @@ -74,9 +71,8 @@ public function execute(InputInterface $input, OutputInterface $output): int { $meta['mimetype'] = $this->mimeTypeDetector->detectString($head); } - $this->writeArrayInOutputFormat($input, $output, $meta); + $output->writeArrayInOutputFormat($meta); - return self::SUCCESS; + return ExitCode::Success; } - } diff --git a/apps/files/lib/Command/Object/ListObject.php b/apps/files/lib/Command/Object/ListObject.php index d72ad3ca77680..248969a055240 100644 --- a/apps/files/lib/Command/Object/ListObject.php +++ b/apps/files/lib/Command/Object/ListObject.php @@ -8,45 +8,43 @@ namespace OCA\Files\Command\Object; -use OC\Core\Command\Base; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IInput; +use OCP\Console\IOutput; use OCP\Files\ObjectStore\IObjectStoreMetaData; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class ListObject extends Base { +#[AsCommand( + name: 'files:object:list', + description: 'List all objects in the object store', +)] +class ListObject { private const CHUNK_SIZE = 100; public function __construct( private readonly ObjectUtil $objectUtils, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - parent::configure(); - $this - ->setName('files:object:list') - ->setDescription('List all objects in the object store') - ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config"); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); + public function __invoke( + IInput $input, + IOutput $output, + #[Option(description: "Bucket to list the objects from, only required in cases where it can't be determined from the config", shortcut: 'b')] ?string $bucket = null, + ): ExitCode { + $objectStore = $this->objectUtils->getObjectStore($bucket, $output); if (!$objectStore) { - return self::FAILURE; + return ExitCode::Failure; } if (!$objectStore instanceof IObjectStoreMetaData) { $output->writeln('Configured object store does currently not support listing objects'); - return self::FAILURE; + return ExitCode::Failure; } $objects = $objectStore->listObjects(); - $objects = $this->objectUtils->formatObjects($objects, $input->getOption('output') === self::OUTPUT_FORMAT_PLAIN); - $this->writeStreamingTableInOutputFormat($input, $output, $objects, self::CHUNK_SIZE); + $objects = $this->objectUtils->formatObjects($objects, $input->getOption('output') === 'plain'); + $output->writeStreamingTableInOutputFormat($objects, self::CHUNK_SIZE); - return self::SUCCESS; + return ExitCode::Success; } } diff --git a/apps/files/lib/Command/Object/Multi/Rename.php b/apps/files/lib/Command/Object/Multi/Rename.php index 92a2718df63bd..bcefe5474a997 100644 --- a/apps/files/lib/Command/Object/Multi/Rename.php +++ b/apps/files/lib/Command/Object/Multi/Rename.php @@ -8,54 +8,48 @@ namespace OCA\Files\Command\Object\Multi; -use OC\Core\Command\Base; use OC\Files\ObjectStore\PrimaryObjectStoreConfig; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\IQuestionHelper; use OCP\IConfig; use OCP\IDBConnection; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; -class Rename extends Base { +#[AsCommand( + name: 'files:object:multi:rename-config', + description: 'Rename an object store configuration and move all users over to the new configuration,', +)] +class Rename { public function __construct( private readonly IDBConnection $connection, private readonly PrimaryObjectStoreConfig $objectStoreConfig, private readonly IConfig $config, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - parent::configure(); - $this - ->setName('files:object:multi:rename-config') - ->setDescription('Rename an object store configuration and move all users over to the new configuration,') - ->addArgument('source', InputArgument::REQUIRED, 'Object store configuration to rename') - ->addArgument('target', InputArgument::REQUIRED, 'New name for the object store configuration'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $source = $input->getArgument('source'); - $target = $input->getArgument('target'); - + public function __invoke( + IOutput $output, + IQuestionHelper $questionHelper, + #[Argument(description: 'Object store configuration to rename')] string $source, + #[Argument(description: 'New name for the object store configuration')] string $target, + ): ExitCode { $configs = $this->objectStoreConfig->getObjectStoreConfigs(); if (!isset($configs[$source])) { $output->writeln('Unknown object store configuration: ' . $source . ''); - return 1; + return ExitCode::Failure; } if ($source === 'root') { $output->writeln('Renaming the root configuration is not supported.'); - return 1; + return ExitCode::Failure; } if ($source === 'default') { $output->writeln('Renaming the default configuration is not supported.'); - return 1; + return ExitCode::Failure; } if (!isset($configs[$target])) { @@ -67,10 +61,8 @@ public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln(''); $output->writeln('Failure to check these requirements will lead to data loss for users.'); - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Automatically create target object store configuration? [y/N] ', false); - if ($helper->ask($input, $output, $question)) { + if ($questionHelper->ask($question)) { $configs[$target] = $configs[$source]; // update all aliases @@ -81,14 +73,14 @@ public function execute(InputInterface $input, OutputInterface $output): int { } $this->config->setSystemValue('objectstore', $configs); } else { - return 0; + return ExitCode::Success; } } elseif (($configs[$source] !== $configs[$target]) || $configs[$source] !== $target) { $output->writeln('Source and target configuration differ.'); $output->writeln(''); $output->writeln('To ensure proper migration of users, the source and target configuration must be the same to ensure that the objects for the moved users exist on the target configuration.'); $output->writeln('The usual migration process consists of creating a clone of the old configuration, moving the users from the old configuration to the new one, and then adjust the old configuration that is longer used.'); - return 1; + return ExitCode::Failure; } $query = $this->connection->getQueryBuilder(); @@ -105,6 +97,6 @@ public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('No users moved'); } - return 0; + return ExitCode::Success; } } diff --git a/apps/files/lib/Command/Object/Multi/Users.php b/apps/files/lib/Command/Object/Multi/Users.php index d33e8d7decaf1..197cf29ff757e 100644 --- a/apps/files/lib/Command/Object/Multi/Users.php +++ b/apps/files/lib/Command/Object/Multi/Users.php @@ -8,47 +8,43 @@ namespace OCA\Files\Command\Object\Multi; -use OC\Core\Command\Base; use OC\Files\ObjectStore\PrimaryObjectStoreConfig; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; use OCP\IConfig; use OCP\IUser; use OCP\IUserManager; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class Users extends Base { +#[AsCommand( + name: 'files:object:multi:users', + description: 'Get the mapping between users and object store buckets', +)] +class Users { public function __construct( private readonly IUserManager $userManager, private readonly PrimaryObjectStoreConfig $objectStoreConfig, private readonly IConfig $config, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - parent::configure(); - $this - ->setName('files:object:multi:users') - ->setDescription('Get the mapping between users and object store buckets') - ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, 'Only list users using the specified bucket') - ->addOption('object-store', 'o', InputOption::VALUE_REQUIRED, 'Only list users using the specified object store configuration') - ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Only show the mapping for the specified user, ignores all other options'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - if ($userId = $input->getOption('user')) { - $user = $this->userManager->get($userId); - if (!$user) { - $output->writeln("User $userId not found"); - return 1; + public function __invoke( + IOutput $output, + #[Option(description: 'Only list users using the specified bucket', shortcut: 'b')] ?string $bucket = null, + #[Option(name: 'object-store', description: 'Only list users using the specified object store configuration', shortcut: 'o')] ?string $objectStore = null, + #[Option(description: 'Only show the mapping for the specified user, ignores all other options', shortcut: 'u')] ?string $user = null, + ): ExitCode { + if ($user) { + $userObject = $this->userManager->get($user); + if (!$userObject) { + $output->writeln("User $user not found"); + return ExitCode::Failure; } - $users = new \ArrayIterator([$user]); + $users = new \ArrayIterator([$userObject]); } else { - $bucket = (string)$input->getOption('bucket'); - $objectStore = (string)$input->getOption('object-store'); + $bucket = (string)$bucket; + $objectStore = (string)$objectStore; if ($bucket !== '' && $objectStore === '') { $users = $this->getUsers($this->config->getUsersForUserValue('homeobjectstore', 'bucket', $bucket)); } elseif ($bucket === '' && $objectStore !== '') { @@ -63,8 +59,8 @@ public function execute(InputInterface $input, OutputInterface $output): int { } } - $this->writeStreamingTableInOutputFormat($input, $output, $this->infoForUsers($users), 100); - return 0; + $output->writeStreamingTableInOutputFormat($this->infoForUsers($users), 100); + return ExitCode::Success; } /** diff --git a/apps/files/lib/Command/Object/ObjectUtil.php b/apps/files/lib/Command/Object/ObjectUtil.php index 5f053c2c42fff..17aba384c1ad4 100644 --- a/apps/files/lib/Command/Object/ObjectUtil.php +++ b/apps/files/lib/Command/Object/ObjectUtil.php @@ -8,12 +8,12 @@ namespace OCA\Files\Command\Object; +use OCP\Console\IOutput; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\ObjectStore\IObjectStore; use OCP\IConfig; use OCP\IDBConnection; use OCP\Util; -use Symfony\Component\Console\Output\OutputInterface; class ObjectUtil { public function __construct( @@ -39,7 +39,7 @@ private function getObjectStoreConfig(): ?array { return null; } - public function getObjectStore(?string $bucket, OutputInterface $output): ?IObjectStore { + public function getObjectStore(?string $bucket, IOutput $output): ?IObjectStore { $config = $this->getObjectStoreConfig(); if (!$config) { $output->writeln('Instance is not using primary object store'); diff --git a/apps/files/lib/Command/Object/Orphans.php b/apps/files/lib/Command/Object/Orphans.php index 0376ac672afb2..bfe060523926e 100644 --- a/apps/files/lib/Command/Object/Orphans.php +++ b/apps/files/lib/Command/Object/Orphans.php @@ -8,15 +8,20 @@ namespace OCA\Files\Command\Object; -use OC\Core\Command\Base; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IInput; +use OCP\Console\IOutput; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\ObjectStore\IObjectStoreMetaData; use OCP\IDBConnection; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class Orphans extends Base { +#[AsCommand( + name: 'files:object:orphans', + description: 'List all objects in the object store that don\'t have a matching entry in the database', +)] +class Orphans { private const CHUNK_SIZE = 100; private ?IQueryBuilder $query = null; @@ -25,7 +30,6 @@ public function __construct( private readonly ObjectUtil $objectUtils, private readonly IDBConnection $connection, ) { - parent::__construct(); } private function getQuery(): IQueryBuilder { @@ -38,25 +42,19 @@ private function getQuery(): IQueryBuilder { return $this->query; } - #[\Override] - protected function configure(): void { - parent::configure(); - $this - ->setName('files:object:orphans') - ->setDescription('List all objects in the object store that don\'t have a matching entry in the database') - ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config"); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); + public function __invoke( + IInput $input, + IOutput $output, + #[Option(description: "Bucket to list the objects from, only required in cases where it can't be determined from the config", shortcut: 'b')] ?string $bucket = null, + ): ExitCode { + $objectStore = $this->objectUtils->getObjectStore($bucket, $output); if (!$objectStore) { - return self::FAILURE; + return ExitCode::Failure; } if (!$objectStore instanceof IObjectStoreMetaData) { $output->writeln('Configured object store does currently not support listing objects'); - return self::FAILURE; + return ExitCode::Failure; } $prefixLength = strlen('urn:oid:'); @@ -66,10 +64,10 @@ public function execute(InputInterface $input, OutputInterface $output): int { return !$this->fileIdInDb($fileId); }); - $orphans = $this->objectUtils->formatObjects($orphans, $input->getOption('output') === self::OUTPUT_FORMAT_PLAIN); - $this->writeStreamingTableInOutputFormat($input, $output, $orphans, self::CHUNK_SIZE); + $orphans = $this->objectUtils->formatObjects($orphans, $input->getOption('output') === 'plain'); + $output->writeStreamingTableInOutputFormat($orphans, self::CHUNK_SIZE); - return self::SUCCESS; + return ExitCode::Success; } private function fileIdInDb(int $fileId): bool { diff --git a/apps/files/lib/Command/Object/Put.php b/apps/files/lib/Command/Object/Put.php index 97bd0009ba05f..729c146b92a8a 100644 --- a/apps/files/lib/Command/Object/Put.php +++ b/apps/files/lib/Command/Object/Put.php @@ -8,63 +8,55 @@ namespace OCA\Files\Command\Object; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\IQuestionHelper; use OCP\Files\IMimeTypeDetector; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; -class Put extends Command { +#[AsCommand( + name: 'files:object:put', + description: 'Write a file to the object store', +)] +class Put { public function __construct( - private ObjectUtil $objectUtils, - private IMimeTypeDetector $mimeTypeDetector, + private readonly ObjectUtil $objectUtils, + private readonly IMimeTypeDetector $mimeTypeDetector, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:object:put') - ->setDescription('Write a file to the object store') - ->addArgument('input', InputArgument::REQUIRED, 'Source local path, use - to read from STDIN') - ->addArgument('object', InputArgument::REQUIRED, 'Object to write') - ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket where to store the object, only required in cases where it can't be determined from the config"); - ; - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $object = $input->getArgument('object'); - $inputName = (string)$input->getArgument('input'); - $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); + public function __invoke( + IOutput $output, + IQuestionHelper $questionHelper, + #[Argument(description: 'Source local path, use - to read from STDIN')] string $input, + #[Argument(description: 'Object to write')] string $object, + #[Option(description: "Bucket where to store the object, only required in cases where it can't be determined from the config", shortcut: 'b')] ?string $bucket = null, + ): ExitCode|int { + $objectStore = $this->objectUtils->getObjectStore($bucket, $output); if (!$objectStore) { return -1; } if ($fileId = $this->objectUtils->objectExistsInDb($object)) { $output->writeln("Warning, object $object belongs to an existing file, overwriting the object contents can lead to unexpected behavior."); - $output->writeln("You can use occ files:put $inputName $fileId to write to the file safely."); + $output->writeln("You can use occ files:put $input $fileId to write to the file safely."); $output->writeln(''); - /** @var QuestionHelper $helper */ - $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Write to the object anyway? [y/N] ', false); - if (!$helper->ask($input, $output, $question)) { + if (!$questionHelper->ask($question)) { return -1; } } - $source = $inputName === '-' ? STDIN : fopen($inputName, 'r'); + $source = $input === '-' ? STDIN : fopen($input, 'r'); if (!$source) { - $output->writeln("Failed to open $inputName"); - return self::FAILURE; + $output->writeln("Failed to open $input"); + return ExitCode::Failure; } - $objectStore->writeObject($object, $source, $this->mimeTypeDetector->detectPath($inputName)); - return self::SUCCESS; + $objectStore->writeObject($object, $source, $this->mimeTypeDetector->detectPath($input)); + return ExitCode::Success; } - } diff --git a/apps/files/lib/Command/Put.php b/apps/files/lib/Command/Put.php index 7907a3c6d2ee0..250970550124f 100644 --- a/apps/files/lib/Command/Put.php +++ b/apps/files/lib/Command/Put.php @@ -9,66 +9,61 @@ namespace OCA\Files\Command; use OC\Core\Command\Info\FileUtils; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -class Put extends Command { +#[AsCommand( + name: 'files:put', + description: 'Write contents of a file', +)] +class Put { public function __construct( - private FileUtils $fileUtils, - private IRootFolder $rootFolder, + private readonly FileUtils $fileUtils, + private readonly IRootFolder $rootFolder, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:put') - ->setDescription('Write contents of a file') - ->addArgument('input', InputArgument::REQUIRED, 'Source local path, use - to read from STDIN') - ->addArgument('file', InputArgument::REQUIRED, 'Target Nextcloud file path to write to or fileid of existing file'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $fileOutput = $input->getArgument('file'); - $inputName = $input->getArgument('input'); - $node = $this->fileUtils->getNode($fileOutput); + public function __invoke( + IOutput $output, + #[Argument(description: 'Source local path, use - to read from STDIN')] string $input, + #[Argument(description: 'Target Nextcloud file path to write to or fileid of existing file')] string $file, + ): ExitCode { + $node = $this->fileUtils->getNode($file); if ($node instanceof Folder) { - $output->writeln("$fileOutput is a folder"); - return self::FAILURE; + $output->writeln("$file is a folder"); + return ExitCode::Failure; } - if (!$node && is_numeric($fileOutput)) { - $output->writeln("$fileOutput not found"); - return self::FAILURE; + if (!$node && is_numeric($file)) { + $output->writeln("$file not found"); + return ExitCode::Failure; } - $source = ($inputName === null || $inputName === '-') ? STDIN : fopen($inputName, 'r'); + $source = ($input === '-') ? STDIN : fopen($input, 'r'); if (!$source) { - $output->writeln("Failed to open $inputName"); - return self::FAILURE; + $output->writeln("Failed to open $input"); + return ExitCode::Failure; } if ($node instanceof File) { $target = $node->fopen('w'); if (!$target) { - $output->writeln("Failed to open $fileOutput"); - return self::FAILURE; + $output->writeln("Failed to open $file"); + return ExitCode::Failure; } stream_copy_to_stream($source, $target); } else { - $parentPath = dirname($fileOutput); + $parentPath = dirname($file); if (!$this->rootFolder->nodeExists($parentPath)) { $this->rootFolder->newFolder($parentPath); } - $this->rootFolder->newFile($fileOutput, $source); + $this->rootFolder->newFile($file, $source); } - return self::SUCCESS; + return ExitCode::Success; } } diff --git a/apps/files/lib/Command/RepairTree.php b/apps/files/lib/Command/RepairTree.php index b7134d02a4dfc..56c0a0be98dd1 100644 --- a/apps/files/lib/Command/RepairTree.php +++ b/apps/files/lib/Command/RepairTree.php @@ -9,39 +9,34 @@ namespace OCA\Files\Command; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\Verbosity; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class RepairTree extends Command { +#[AsCommand( + name: 'files:repair-tree', + description: 'Try and repair malformed filesystem tree structures (may be necessary to run multiple times for nested malformations)', +)] +class RepairTree { public const CHUNK_SIZE = 200; public function __construct( - protected IDBConnection $connection, + private readonly IDBConnection $connection, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:repair-tree') - ->setDescription('Try and repair malformed filesystem tree structures (may be necessary to run multiple times for nested malformations)') - ->addOption('dry-run') - ->addOption('storage-id', 's', InputOption::VALUE_OPTIONAL, 'If set, only repair files within the given storage numeric ID', null) - ->addOption('path', 'p', InputOption::VALUE_OPTIONAL, 'If set, only repair files within the given path', null); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $rows = $this->findBrokenTreeBits( - $input->getOption('storage-id'), - $input->getOption('path'), - ); - $fix = !$input->getOption('dry-run'); + public function __invoke( + IOutput $output, + #[Option] bool $dryRun = false, + #[Option(name: 'storage-id', description: 'If set, only repair files within the given storage numeric ID', shortcut: 's')] ?string $storageId = null, + #[Option(description: 'If set, only repair files within the given path', shortcut: 'p')] ?string $path = null, + ): ExitCode { + $rows = $this->findBrokenTreeBits($storageId, $path); + $fix = !$dryRun; $output->writeln('Found ' . count($rows) . ' file entries with an invalid path'); @@ -57,7 +52,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { ->where($query->expr()->eq('fileid', $query->createParameter('fileid'))); foreach ($rows as $row) { - $output->writeln("Path of file {$row['fileid']} is {$row['path']} but should be {$row['parent_path']}/{$row['name']} based on its parent", OutputInterface::VERBOSITY_VERBOSE); + $output->writeln("Path of file {$row['fileid']} is {$row['path']} but should be {$row['parent_path']}/{$row['name']} based on its parent", Verbosity::Verbose); if ($fix) { $fileId = $this->getFileId((int)$row['parent_storage'], $row['parent_path'] . '/' . $row['name']); @@ -79,7 +74,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { $this->connection->commit(); } - return self::SUCCESS; + return ExitCode::Success; } private function getFileId(int $storage, string $path) { diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php index 3432cc835a976..6734bab068cc7 100644 --- a/apps/files/lib/Command/SanitizeFilenames.php +++ b/apps/files/lib/Command/SanitizeFilenames.php @@ -10,10 +10,15 @@ namespace OCA\Files\Command; use Exception; -use OC\Core\Command\Base; use OC\Files\FilenameValidator; use OCA\Files\Service\SettingsService; use OCP\AppFramework\Services\IAppConfig; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\Verbosity; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; @@ -22,59 +27,39 @@ use OCP\IUserSession; use OCP\L10N\IFactory; use OCP\Lock\LockedException; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class SanitizeFilenames extends Base { +#[AsCommand( + name: 'files:sanitize-filenames', + description: 'Renames files to match naming constraints', +)] +class SanitizeFilenames { - private OutputInterface $output; + private IOutput $output; private ?string $charReplacement; private bool $dryRun; private bool $errorsOrSkipped = false; public function __construct( - private IUserManager $userManager, - private IRootFolder $rootFolder, - private IUserSession $session, - private IFactory $l10nFactory, - private FilenameValidator $filenameValidator, - private SettingsService $service, - private IAppConfig $appConfig, + private readonly IUserManager $userManager, + private readonly IRootFolder $rootFolder, + private readonly IUserSession $session, + private readonly IFactory $l10nFactory, + private readonly FilenameValidator $filenameValidator, + private readonly SettingsService $service, + private readonly IAppConfig $appConfig, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - parent::configure(); - - $this - ->setName('files:sanitize-filenames') - ->setDescription('Renames files to match naming constraints') - ->addArgument( - 'user_id', - InputArgument::OPTIONAL | InputArgument::IS_ARRAY, - 'will only rename files the given user(s) have access to' - ) - ->addOption( - 'dry-run', - mode: InputOption::VALUE_NONE, - description: 'Do not actually rename any files but just check filenames.', - ) - ->addOption( - 'char-replacement', - 'c', - mode: InputOption::VALUE_REQUIRED, - description: 'Replacement for invalid character (by default space, underscore or dash is used)', - ); - - } - - #[\Override] - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->charReplacement = $input->getOption('char-replacement'); + public function __invoke( + IOutput $output, + #[Argument(name: 'user_id', description: 'will only rename files the given user(s) have access to')] + array $userIds = [], + #[Option(name: 'dry-run', description: 'Do not actually rename any files but just check filenames.')] + bool $dryRun = false, + #[Option(name: 'char-replacement', description: 'Replacement for invalid character (by default space, underscore or dash is used)', shortcut: 'c')] + ?string $charReplacement = null, + ): ExitCode { + $this->charReplacement = $charReplacement; // check if replacement is needed $c = $this->filenameValidator->getForbiddenCharacters(); if (count($c) > 0) { @@ -86,19 +71,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int } else { $output->writeln('Invalid character replacement given'); } - return 1; + return ExitCode::Failure; } } - $this->dryRun = $input->getOption('dry-run'); + $this->dryRun = $dryRun; if ($this->dryRun) { $output->writeln('Dry run is enabled, no actual renaming will be applied.'); } $this->output = $output; - $users = $input->getArgument('user_id'); - if (!empty($users)) { - foreach ($users as $userId) { + if (!empty($userIds)) { + foreach ($userIds as $userId) { $user = $this->userManager->get($userId); if ($user === null) { $output->writeln("User '$userId' does not exist - skipping"); @@ -113,7 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_DONE); } } - return self::SUCCESS; + return ExitCode::Success; } private function sanitizeUserFiles(IUser $user): void { @@ -128,7 +112,7 @@ private function sanitizeUserFiles(IUser $user): void { private function sanitizeFiles(Folder $folder): void { foreach ($folder->getDirectoryListing() as $node) { - $this->output->writeln('scanning: ' . $node->getPath(), OutputInterface::VERBOSITY_VERBOSE); + $this->output->writeln('scanning: ' . $node->getPath(), Verbosity::Verbose); try { $oldName = $node->getName(); @@ -151,7 +135,7 @@ private function sanitizeFiles(Folder $folder): void { $this->output->writeln('skipping: ' . $node->getPath() . ' (no permissions)'); } catch (Exception $error) { $this->output->writeln('failed: ' . $node->getPath() . ''); - $this->output->writeln('' . $error->getMessage() . '', OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_VERBOSE); + $this->output->writeln('' . $error->getMessage() . '', Verbosity::Verbose); } if ($node instanceof Folder) { @@ -159,5 +143,4 @@ private function sanitizeFiles(Folder $folder): void { } } } - } diff --git a/apps/files/lib/Command/Touch.php b/apps/files/lib/Command/Touch.php index 217ce721aa4b4..65cedc18a5a35 100644 --- a/apps/files/lib/Command/Touch.php +++ b/apps/files/lib/Command/Touch.php @@ -10,46 +10,43 @@ use DateTimeImmutable; use OC\Core\Command\Info\FileUtils; +use OCP\Console\Attribute\Argument; +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; use OCP\Files\IRootFolder; use Psr\Clock\ClockInterface; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -class Touch extends Command { +#[AsCommand( + name: 'files:touch', + description: 'Update the last modified date of a file or folder, or create an empty file', +)] +class Touch { public function __construct( private readonly FileUtils $fileUtils, private readonly IRootFolder $rootFolder, private readonly ClockInterface $clock, ) { - parent::__construct(); } - #[\Override] - protected function configure(): void { - $this - ->setName('files:touch') - ->setDescription('Update the last modified date of a file or folder, or create an empty file') - ->addArgument('file', InputArgument::REQUIRED, 'Nextcloud path or fileid for the file or folder to change the modified date of') - ->addOption('date', 'd', InputOption::VALUE_REQUIRED, 'Time to use as modified date instead of the current time. Acceptable formats are: ISO8601, "YYYY-MM-DD" and Unix time in seconds.') - ->addOption('no-create', 'c', InputOption::VALUE_NONE, 'Don\'t create an empty file if the target path doesn\'t exist'); - } - - #[\Override] - public function execute(InputInterface $input, OutputInterface $output): int { - $fileInput = $input->getArgument('file'); - $node = $this->fileUtils->getNode($fileInput); - $date = $input->getOption('date'); - $noCreate = $input->getOption('no-create'); + public function __invoke( + IOutput $output, + #[Argument(description: 'Nextcloud path or fileid for the file or folder to change the modified date of')] + string $file, + #[Option(name: 'no-create', description: 'Don\'t create an empty file if the target path doesn\'t exist', shortcut: 'c')] + bool $noCreate = false, + #[Option(description: 'Time to use as modified date instead of the current time. Acceptable formats are: ISO8601, "YYYY-MM-DD" and Unix time in seconds.', shortcut: 'd')] + ?string $date = null, + ): ExitCode { + $node = $this->fileUtils->getNode($file); if (!$node) { - if ($noCreate || is_numeric($fileInput)) { - $output->writeln("$fileInput doesn't exist"); - return self::FAILURE; + if ($noCreate || is_numeric($file)) { + $output->writeln("$file doesn't exist"); + return ExitCode::Failure; } - $node = $this->rootFolder->newFile($fileInput); + $node = $this->rootFolder->newFile($file); } if ($date) { @@ -62,15 +59,13 @@ public function execute(InputInterface $input, OutputInterface $output): int { } $node->touch($mtime->getTimestamp()); - return self::SUCCESS; + return ExitCode::Success; } /** * @return \DateTimeImmutable|false */ protected function parseDateOption(string $input) { - $date = false; - // Handle Unix timestamp if (filter_var($input, FILTER_VALIDATE_INT)) { return new DateTimeImmutable('@' . $input); diff --git a/apps/files/lib/Command/WindowsCompatibleFilenames.php b/apps/files/lib/Command/WindowsCompatibleFilenames.php index d595d5731b68b..86e59fe952d57 100644 --- a/apps/files/lib/Command/WindowsCompatibleFilenames.php +++ b/apps/files/lib/Command/WindowsCompatibleFilenames.php @@ -9,47 +9,43 @@ namespace OCA\Files\Command; -use OC\Core\Command\Base; use OCA\Files\Service\SettingsService; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class WindowsCompatibleFilenames extends Base { - +use OCP\Console\Attribute\AsCommand; +use OCP\Console\Attribute\Option; +use OCP\Console\ExitCode; +use OCP\Console\IOutput; +use OCP\Console\Verbosity; + +#[AsCommand( + name: 'files:windows-compatible-filenames', + description: 'Enforce naming constraints for windows compatible filenames', +)] +class WindowsCompatibleFilenames { public function __construct( - private SettingsService $service, + private readonly SettingsService $service, ) { - parent::__construct(); - } - - #[\Override] - protected function configure(): void { - parent::configure(); - - $this - ->setName('files:windows-compatible-filenames') - ->setDescription('Enforce naming constraints for windows compatible filenames') - ->addOption('enable', description: 'Enable windows naming constraints') - ->addOption('disable', description: 'Disable windows naming constraints'); } - #[\Override] - protected function execute(InputInterface $input, OutputInterface $output): int { - if ($input->getOption('enable')) { + public function __invoke( + IOutput $output, + #[Option(description: 'Enable windows naming constraints')] bool $enable = false, + #[Option(description: 'Disable windows naming constraints')] bool $disable = false, + ): ExitCode { + if ($enable) { if ($this->service->hasFilesWindowsSupport()) { - $output->writeln('Windows compatible filenames already enforced.', OutputInterface::VERBOSITY_VERBOSE); + $output->writeln('Windows compatible filenames already enforced.', Verbosity::Verbose); } $this->service->setFilesWindowsSupport(true); $output->writeln('Windows compatible filenames enforced.'); - } elseif ($input->getOption('disable')) { + } elseif ($disable) { if (!$this->service->hasFilesWindowsSupport()) { - $output->writeln('Windows compatible filenames already disabled.', OutputInterface::VERBOSITY_VERBOSE); + $output->writeln('Windows compatible filenames already disabled.', Verbosity::Verbose); } $this->service->setFilesWindowsSupport(false); $output->writeln('Windows compatible filename constraints removed.'); } else { $output->writeln('Windows compatible filenames are ' . ($this->service->hasFilesWindowsSupport() ? 'enforced' : 'disabled')); } - return self::SUCCESS; + return ExitCode::Success; } } diff --git a/apps/files/tests/Command/DeleteOrphanedFilesTest.php b/apps/files/tests/Command/DeleteOrphanedFilesTest.php index f9db250eeabb4..4745669cebeec 100644 --- a/apps/files/tests/Command/DeleteOrphanedFilesTest.php +++ b/apps/files/tests/Command/DeleteOrphanedFilesTest.php @@ -11,13 +11,12 @@ use OC\Files\View; use OCA\Files\Command\DeleteOrphanedFiles; +use OCP\Console\IOutput; use OCP\Files\IRootFolder; use OCP\Files\StorageNotAvailableException; use OCP\IDBConnection; use OCP\IUserManager; use OCP\Server; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Test\TestCase; /** @@ -78,8 +77,7 @@ protected function getMountsCount(int $storageId): int { * Test clearing orphaned files */ public function testClearFiles(): void { - $input = $this->createMock(InputInterface::class); - $output = $this->createMock(OutputInterface::class); + $output = $this->createMock(IOutput::class); $rootFolder = Server::get(IRootFolder::class); @@ -99,7 +97,7 @@ public function testClearFiles(): void { $this->assertCount(1, $this->getFile($fileInfo->getId()), 'Asserts that file is available'); $this->assertEquals(1, $this->getMountsCount($numericStorageId), 'Asserts that mount is available'); - $this->command->execute($input, $output); + ($this->command)($output); $this->assertCount(1, $this->getFile($fileInfo->getId()), 'Asserts that file is still available'); $this->assertEquals(1, $this->getMountsCount($numericStorageId), 'Asserts that mount is still available'); @@ -125,7 +123,7 @@ public function testClearFiles(): void { $this->assertSame($expected, $message); }); - $this->command->execute($input, $output); + ($this->command)($output); $this->assertCount(0, $this->getFile($fileInfo->getId()), 'Asserts that file gets cleaned up'); $this->assertEquals(0, $this->getMountsCount($numericStorageId), 'Asserts that mount gets cleaned up'); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b23819d093969..05451a516f5f8 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -335,7 +335,14 @@ 'OCP\\Config\\Lexicon\\Preset' => $baseDir . '/lib/public/Config/Lexicon/Preset.php', 'OCP\\Config\\Lexicon\\Strictness' => $baseDir . '/lib/public/Config/Lexicon/Strictness.php', 'OCP\\Config\\ValueType' => $baseDir . '/lib/public/Config/ValueType.php', + 'OCP\\Console\\Attribute\\Argument' => $baseDir . '/lib/public/Console/Attribute/Argument.php', + 'OCP\\Console\\Attribute\\AsCommand' => $baseDir . '/lib/public/Console/Attribute/AsCommand.php', + 'OCP\\Console\\Attribute\\Option' => $baseDir . '/lib/public/Console/Attribute/Option.php', 'OCP\\Console\\ConsoleEvent' => $baseDir . '/lib/public/Console/ConsoleEvent.php', + 'OCP\\Console\\ExitCode' => $baseDir . '/lib/public/Console/ExitCode.php', + 'OCP\\Console\\IInput' => $baseDir . '/lib/public/Console/IInput.php', + 'OCP\\Console\\IOutput' => $baseDir . '/lib/public/Console/IOutput.php', + 'OCP\\Console\\IQuestionHelper' => $baseDir . '/lib/public/Console/IQuestionHelper.php', 'OCP\\Console\\ReservedOptions' => $baseDir . '/lib/public/Console/ReservedOptions.php', 'OCP\\Constants' => $baseDir . '/lib/public/Constants.php', 'OCP\\Contacts\\ContactsMenu\\IAction' => $baseDir . '/lib/public/Contacts/ContactsMenu/IAction.php', @@ -1361,6 +1368,11 @@ 'OC\\Config\\PresetManager' => $baseDir . '/lib/private/Config/PresetManager.php', 'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php', 'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php', + 'OC\\Console\\CommandAdapter' => $baseDir . '/lib/private/Console/CommandAdapter.php', + 'OC\\Console\\InputAdapter' => $baseDir . '/lib/private/Console/InputAdapter.php', + 'OC\\Console\\OutputAdapter' => $baseDir . '/lib/private/Console/OutputAdapter.php', + 'OC\\Console\\QuestionHelperAdapter' => $baseDir . '/lib/private/Console/QuestionHelperAdapter.php', + 'OC\\Console\\ReflectionMember' => $baseDir . '/lib/private/Console/ReflectionMember.php', 'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => $baseDir . '/lib/private/ContactsManager.php', 'OC\\Contacts\\ContactsMenu\\ActionFactory' => $baseDir . '/lib/private/Contacts/ContactsMenu/ActionFactory.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1c629facd2da4..d912ca35010a1 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -376,7 +376,14 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Config\\Lexicon\\Preset' => __DIR__ . '/../../..' . '/lib/public/Config/Lexicon/Preset.php', 'OCP\\Config\\Lexicon\\Strictness' => __DIR__ . '/../../..' . '/lib/public/Config/Lexicon/Strictness.php', 'OCP\\Config\\ValueType' => __DIR__ . '/../../..' . '/lib/public/Config/ValueType.php', + 'OCP\\Console\\Attribute\\Argument' => __DIR__ . '/../../..' . '/lib/public/Console/Attribute/Argument.php', + 'OCP\\Console\\Attribute\\AsCommand' => __DIR__ . '/../../..' . '/lib/public/Console/Attribute/AsCommand.php', + 'OCP\\Console\\Attribute\\Option' => __DIR__ . '/../../..' . '/lib/public/Console/Attribute/Option.php', 'OCP\\Console\\ConsoleEvent' => __DIR__ . '/../../..' . '/lib/public/Console/ConsoleEvent.php', + 'OCP\\Console\\ExitCode' => __DIR__ . '/../../..' . '/lib/public/Console/ExitCode.php', + 'OCP\\Console\\IInput' => __DIR__ . '/../../..' . '/lib/public/Console/IInput.php', + 'OCP\\Console\\IOutput' => __DIR__ . '/../../..' . '/lib/public/Console/IOutput.php', + 'OCP\\Console\\IQuestionHelper' => __DIR__ . '/../../..' . '/lib/public/Console/IQuestionHelper.php', 'OCP\\Console\\ReservedOptions' => __DIR__ . '/../../..' . '/lib/public/Console/ReservedOptions.php', 'OCP\\Constants' => __DIR__ . '/../../..' . '/lib/public/Constants.php', 'OCP\\Contacts\\ContactsMenu\\IAction' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IAction.php', @@ -1402,6 +1409,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Config\\PresetManager' => __DIR__ . '/../../..' . '/lib/private/Config/PresetManager.php', 'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php', 'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php', + 'OC\\Console\\CommandAdapter' => __DIR__ . '/../../..' . '/lib/private/Console/CommandAdapter.php', + 'OC\\Console\\InputAdapter' => __DIR__ . '/../../..' . '/lib/private/Console/InputAdapter.php', + 'OC\\Console\\OutputAdapter' => __DIR__ . '/../../..' . '/lib/private/Console/OutputAdapter.php', + 'OC\\Console\\QuestionHelperAdapter' => __DIR__ . '/../../..' . '/lib/private/Console/QuestionHelperAdapter.php', + 'OC\\Console\\ReflectionMember' => __DIR__ . '/../../..' . '/lib/private/Console/ReflectionMember.php', 'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => __DIR__ . '/../../..' . '/lib/private/ContactsManager.php', 'OC\\Contacts\\ContactsMenu\\ActionFactory' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/ActionFactory.php', diff --git a/lib/private/Console/Application.php b/lib/private/Console/Application.php index e18dcc5ff8ef0..55953ae0655ca 100644 --- a/lib/private/Console/Application.php +++ b/lib/private/Console/Application.php @@ -14,6 +14,7 @@ use OC\SystemConfig; use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; +use OCP\Console\Attribute\AsCommand; use OCP\Console\ConsoleEvent; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; @@ -215,6 +216,25 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu */ private function loadCommandsFromInfoXml(iterable $commands): void { foreach ($commands as $command) { + if (class_exists($command)) { + $reflectionClass = new \ReflectionClass($command); + if ($reflectionClass->getAttributes(AsCommand::class) !== []) { + $this->application->add(new CommandAdapter($command, null, \OC::$server)); + continue; + } + + $hasMethodCommands = false; + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { + if ($reflectionMethod->getAttributes(AsCommand::class) !== []) { + $this->application->add(new CommandAdapter($command, $reflectionMethod->getName(), \OC::$server)); + $hasMethodCommands = true; + } + } + if ($hasMethodCommands) { + continue; + } + } + try { $c = Server::get($command); } catch (ContainerExceptionInterface $e) { diff --git a/lib/private/Console/CommandAdapter.php b/lib/private/Console/CommandAdapter.php new file mode 100644 index 0000000000000..cbda962d56755 --- /dev/null +++ b/lib/private/Console/CommandAdapter.php @@ -0,0 +1,234 @@ + */ + private array $arguments = []; + + /** @var array */ + private array $options = []; + + /** + * @param class-string $className + */ + public function __construct( + private readonly string $className, + private readonly ?string $method, + private readonly ContainerInterface $container, + ) { + + if ($method !== null) { + $reflectionMethod = new \ReflectionMethod($className, $method); + $asCommands = $reflectionMethod->getAttributes(AsCommand::class); + if ($asCommands === []) { + throw new \RuntimeException('Missing #[AsCommand] attribute on method: ' . $method . ' from class: ' . $className); + } + + $this->asCommand = $asCommands[0]->newInstance(); + } else { + $reflectionClass = new \ReflectionClass($className); + $asCommands = $reflectionClass->getAttributes(AsCommand::class); + if ($asCommands === []) { + throw new \RuntimeException('Missing #[AsCommand] attribute on class: ' . $className); + } + + $this->asCommand = $asCommands[0]->newInstance(); + + $reflectionMethod = new \ReflectionMethod($className, '__invoke'); + } + + $this->reflectionMethod = $reflectionMethod; + + foreach ($reflectionMethod->getParameters() as $parameter) { + $args = $parameter->getAttributes(Argument::class); + if ($args !== []) { + /** @var Argument $argument */ + $argument = $args[0]->newInstance(); + if ($argument->name === '') { + $argument->name = $parameter->getName(); + } + + $this->arguments[$parameter->getName()] = ['arg' => $argument, 'parameter' => $parameter]; + } + + $args = $parameter->getAttributes(Option::class); + if ($args !== []) { + /** @var Option $option */ + $option = $args[0]->newInstance(); + if ($option->name === '') { + $option->name = $parameter->getName(); + } + + $this->options[$parameter->getName()] = ['option' => $option, 'parameter' => $parameter]; + } + } + + parent::__construct(); + } + + #[Override] + public function configure(): void { + parent::configure(); + + $this->setName($this->asCommand->name); + + if ($this->asCommand->description) { + $this->setDescription($this->asCommand->description); + } + + foreach ($this->arguments as $argument) { + /** @var Argument $arg */ + $arg = $argument['arg']; + $parameter = $argument['parameter']; + $reflection = new ReflectionMember($parameter); + $type = $reflection->getType(); + $name = $reflection->getName(); + if (!$type instanceof \ReflectionNamedType) { + throw new \LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $reflection->getMemberName(), $name, $reflection->getSourceName())); + } + $isOptional = $reflection->hasDefaultValue() || $reflection->isNullable() || $reflection->isVariadic(); + $typeName = $type->getName(); + $mode = $isOptional ? InputArgument::OPTIONAL : InputArgument::REQUIRED; + if ($typeName === 'array' || $reflection->isVariadic()) { + $mode |= InputArgument::IS_ARRAY; + } + $default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null; + + $this->addArgument($arg->name, $mode, $arg->description, $default); + } + + foreach ($this->options as $option) { + $parameter = $option['parameter']; + /** @var Option $option */ + $option = $option['option']; + $reflection = new ReflectionMember($parameter); + $type = $reflection->getType(); + $name = $reflection->getName(); + if (!$type instanceof \ReflectionNamedType) { + throw new \LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for command options.', $reflection->getMemberName(), $name, $reflection->getSourceName())); + } + $allowNull = $reflection->isNullable(); + $default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null; + $typeName = $type->getName(); + + if ($typeName === 'bool' && $allowNull && \in_array($default, [true, false], true)) { + throw new \LogicException(\sprintf('The option %s "$%s" of "%s" must not be nullable when it has a default boolean value.', $reflection->getMemberName(), $name, $reflection->getSourceName())); + } + + if ($allowNull && $default !== null) { + throw new \LogicException(\sprintf('The option %s "$%s" of "%s" must either be not-nullable or have a default of null.', $reflection->getMemberName(), $name, $reflection->getSourceName())); + } + + if ($typeName === 'bool') { + $mode = InputOption::VALUE_NONE; + if ($default !== false) { + $mode |= InputOption::VALUE_NEGATABLE; + } else { + $default = null; + } + } elseif ($typeName === 'array' || $reflection->isVariadic()) { + $mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY; + } else { + $mode = InputOption::VALUE_REQUIRED; + } + + $this->addOption($option->name, $option->shortcut, $mode, $option->description, $default); + } + } + + #[Override] + public function execute(InputInterface $input, OutputInterface $output): int { + /** @var T $instance */ + $instance = $this->container->get($this->className); + + $parameters = []; + foreach ($this->reflectionMethod->getParameters() as $parameter) { + $name = $parameter->getName(); + + if (isset($this->arguments[$name])) { + $parameters[] = $input->getArgument($this->arguments[$name]['arg']->name); + continue; + } + + if (isset($this->options[$name])) { + $parameters[] = $input->getOption($this->options[$name]['option']->name); + continue; + } + + $type = $parameter->getType(); + if ($type instanceof \ReflectionNamedType && $type->getName() === IOutput::class) { + $parameters[] = new OutputAdapter($output, $input, $this); + continue; + } + + if ($type instanceof \ReflectionNamedType && $type->getName() === IInput::class) { + $parameters[] = new InputAdapter($input); + continue; + } + + if ($type instanceof \ReflectionNamedType && $type->getName() === IQuestionHelper::class) { + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $parameters[] = new QuestionHelperAdapter($input, $output, $questionHelper); + continue; + } + + throw new \LogicException(\sprintf('Unable to resolve parameter "$%s" of "%s": it is neither an #[Argument], an #[Option], nor an %s, %s or %s.', $name, $this->reflectionMethod->getName(), IOutput::class, IInput::class, IQuestionHelper::class)); + } + + if ($this->method !== null) { + $result = $instance->{$this->method}(...$parameters); + } else { + $result = $instance(...$parameters); + } + + return $result instanceof ExitCode ? $result->value : $result; + } + + #[Override] + public function writeArrayInOutputFormat(InputInterface $input, OutputInterface $output, iterable $items, string $prefix = ' - '): void { + // To make it public + parent::writeArrayInOutputFormat($input, $output, $items, $prefix); + } + + #[Override] + public function writeTableInOutputFormat(InputInterface $input, OutputInterface $output, array $items): void { + // To make it public + parent::writeTableInOutputFormat($input, $output, $items); + } + + #[Override] + public function writeStreamingTableInOutputFormat(InputInterface $input, OutputInterface $output, \Iterator $items, int $tableGroupSize): void { + // To make it public + parent::writeStreamingTableInOutputFormat($input, $output, $items, $tableGroupSize); + } +} diff --git a/lib/private/Console/InputAdapter.php b/lib/private/Console/InputAdapter.php new file mode 100644 index 0000000000000..9dc8063ff92ba --- /dev/null +++ b/lib/private/Console/InputAdapter.php @@ -0,0 +1,49 @@ +input->getArguments(); + } + + #[Override] + public function getArgument(string $name): string|bool|int|float|array|null { + return $this->input->getArgument($name); + } + + #[Override] + public function hasArgument(string $name): bool { + return $this->input->hasArgument($name); + } + + #[Override] + public function getOptions(): array { + return $this->input->getOptions(); + } + + #[Override] + public function getOption(string $name): string|bool|int|float|array|null { + return $this->input->getOption($name); + } + + #[Override] + public function hasOption(string $name): bool { + return $this->input->hasOption($name); + } +} diff --git a/lib/private/Console/OutputAdapter.php b/lib/private/Console/OutputAdapter.php new file mode 100644 index 0000000000000..7837f8eeb2830 --- /dev/null +++ b/lib/private/Console/OutputAdapter.php @@ -0,0 +1,78 @@ +output->write($messages, $newline, $verbosity->value); + } + + #[Override] + public function writeln(iterable|string $messages, Verbosity $verbosity = Verbosity::Normal): void { + $this->output->writeln($messages, $verbosity->value); + } + + #[Override] + public function isQuiet(): bool { + return $this->output->isQuiet(); + } + + #[Override] + public function isVerbose(): bool { + return $this->output->isVerbose(); + } + + #[Override] + public function isVeryVerbose(): bool { + return $this->output->isVeryVerbose(); + } + + #[Override] + public function isDebug(): bool { + return $this->output->isDebug(); + } + + #[Override] + public function writeArrayInOutputFormat(iterable $items, string $prefix = ' - '): void { + $this->commandAdapter->writeArrayInOutputFormat($this->input, $this->output, $items, $prefix); + } + + #[Override] + public function writeTableInOutputFormat(array $items): void { + $this->commandAdapter->writeTableInOutputFormat($this->input, $this->output, $items); + } + + #[Override] + public function writeStreamingTableInOutputFormat(\Iterator $items, int $tableGroupSize): void { + $this->commandAdapter->writeStreamingTableInOutputFormat($this->input, $this->output, $items, $tableGroupSize); + } + + #[Override] + public function setVerbosity(Verbosity $level): void { + $this->output->setVerbosity($level->value); + } + + #[Override] + public function getVerbosity(): Verbosity { + return Verbosity::from($this->output->getVerbosity()); + } +} diff --git a/lib/private/Console/QuestionHelperAdapter.php b/lib/private/Console/QuestionHelperAdapter.php new file mode 100644 index 0000000000000..6ecc147cd39c7 --- /dev/null +++ b/lib/private/Console/QuestionHelperAdapter.php @@ -0,0 +1,28 @@ +questionHelper->ask($this->input, $this->output, $question); + } +} diff --git a/lib/private/Console/ReflectionMember.php b/lib/private/Console/ReflectionMember.php new file mode 100644 index 0000000000000..cea6200f20b97 --- /dev/null +++ b/lib/private/Console/ReflectionMember.php @@ -0,0 +1,115 @@ + +// SPDX-License-Identifier: MIT + +namespace OC\Console; + +/** + * @internal + */ +class ReflectionMember { + public function __construct( + private readonly \ReflectionParameter|\ReflectionProperty $member, + ) { + } + + /** + * @template T of object + * + * @param class-string $class + * + * @return T|null + */ + public function getAttribute(string $class): ?object { + return ($this->member->getAttributes($class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance(); + } + + /** + * @template T of object + * + * @param class-string $class + * + * @return list + */ + public function getAttributes(string $class): array { + return array_map( + static fn (\ReflectionAttribute $attribute) => $attribute->newInstance(), + $this->member->getAttributes($class, \ReflectionAttribute::IS_INSTANCEOF) + ); + } + + public function getSourceName(): string { + if ($this->member instanceof \ReflectionProperty) { + return $this->member->class; + } + + $function = $this->member->getDeclaringFunction(); + + if ($function instanceof \ReflectionMethod) { + return $function->class . '::' . $function->name . '()'; + } + + return $function->name . '()'; + } + + public function getSourceThis(): ?object { + if ($this->member instanceof \ReflectionParameter) { + return $this->member->getDeclaringFunction()->getClosureThis(); + } + + return null; + } + + public function getType(): ?\ReflectionType { + return $this->member->getType(); + } + + public function getName(): string { + return $this->member->getName(); + } + + public function hasDefaultValue(): bool { + if ($this->member instanceof \ReflectionParameter) { + return $this->member->isDefaultValueAvailable(); + } + + return $this->member->hasDefaultValue(); + } + + public function getDefaultValue(): mixed { + $defaultValue = $this->member->getDefaultValue(); + + if ($defaultValue instanceof \BackedEnum) { + return $defaultValue->value; + } + + return $defaultValue; + } + + public function isNullable(): bool { + return (bool)$this->member->getType()?->allowsNull(); + } + + public function getMemberName(): string { + return $this->member instanceof \ReflectionParameter ? 'parameter' : 'property'; + } + + public function isParameter(): bool { + return $this->member instanceof \ReflectionParameter; + } + + public function isVariadic(): bool { + return $this->member instanceof \ReflectionParameter && $this->member->isVariadic(); + } + + public function isProperty(): bool { + return $this->member instanceof \ReflectionProperty; + } + + public function getMember(): \ReflectionParameter|\ReflectionProperty { + return $this->member; + } +} diff --git a/lib/public/Console/Attribute/Argument.php b/lib/public/Console/Attribute/Argument.php new file mode 100644 index 0000000000000..f7f590d5f5996 --- /dev/null +++ b/lib/public/Console/Attribute/Argument.php @@ -0,0 +1,73 @@ + definition. + * + * Can be used in the parameters of the invoke method. + * + * ``` + * #[AsCommand(name: 'app:user:created') + * class CreateUserCommand { + * public function __invoke( + * #[Argument(description: "The username of the user")] string $userId, + * IOutput $output, + * ): ExitCode { + * // ... + * return ExitCode::Success; + * } + * } + * ``` + * + * Or on methods parameters: + * + * ``` + * class UserCommands { + * #[AsCommand('app:user:create')] + * public function create( + * #[Argument(description: "The username of the user")] string $userId, + * IOutput $output, + * ): ExitCode { + * // ... + * + * return ExitCode::Success; + * } + * + * #[AsCommand('app:user:delete')] + * public function delete( + * #[Argument(description: "The username of the user")] string $userId, + * IOutput $output, + * ): ExitCode { + * // ... + * + * return ExitCode::Success; + * } + * } + * ``` + * + * @since 35.0.0 + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +#[Consumable(since: '35.0.0')] +final class Argument { + /** + * If unset, the `name` value will be inferred from the parameter definition. + * + * @param string $description The description of the argument, displayed with the help page + * @param string $name The name of the argument + * @since 35.0.0 + */ + public function __construct( + public string $description = '', + public string $name = '', + ) { + } +} diff --git a/lib/public/Console/Attribute/AsCommand.php b/lib/public/Console/Attribute/AsCommand.php new file mode 100644 index 0000000000000..7a1481d5c3ccf --- /dev/null +++ b/lib/public/Console/Attribute/AsCommand.php @@ -0,0 +1,77 @@ + definition. + * + * Can be used in the parameters of the invoke method. + * + * ``` + * #[AsCommand(name: 'app:user:created') + * class CreateUserCommand { + * public function __invoke( + * #[Option(description: "The username of the user")] string $userId, + * IOutput $output, + * ): ExitCode { + * // ... + * return ExitCode::Success; + * } + * } + * ``` + * + * Or on methods parameters: + * + * ``` + * class UserCommands { + * #[AsCommand('app:user:create')] + * public function create( + * #[Option(description: "The username of the user")] string $userId, + * IOutput $output, + * ): ExitCode { + * // ... + * + * return ExitCode::Success; + * } + * + * #[AsCommand('app:user:delete')] + * public function delete( + * #[Option(description: "The username of the user")] string $userId, + * IOutput $output, + * ): ExitCode { + * // ... + * + * return ExitCode::Success; + * } + * } + * ``` + * + * @since 35.0.0 + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +#[Consumable(since: '35.0.0')] +final class Option { + /** + * If unset, the `name` value will be inferred from the parameter definition. + * + * @param string $description The description of the option, displayed with the help page + * @param string $name The name of the option + * @param array|string|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @since 35.0.0 + */ + public function __construct( + public string $description = '', + public string $name = '', + public array|string|null $shortcut = null, + ) { + } +} diff --git a/lib/public/Console/ExitCode.php b/lib/public/Console/ExitCode.php new file mode 100644 index 0000000000000..3666bcd05ded4 --- /dev/null +++ b/lib/public/Console/ExitCode.php @@ -0,0 +1,24 @@ + + * @since 35.0.0 + */ + public function getArguments(): array; + + /** + * Returns the argument value for a given argument name. + * + * @throws InvalidArgumentException When argument given doesn't exist + * @since 35.0.0 + */ + public function getArgument(string $name): string|bool|int|float|array|null; + + /** + * Returns true if an argument exists by name or position. + * @since 35.0.0 + */ + public function hasArgument(string $name): bool; + + /** + * Returns all the given options merged with the default values. + * + * @return array + * @since 35.0.0 + */ + public function getOptions(): array; + + /** + * Returns the option value for a given option name. + * + * @throws InvalidArgumentException When option given doesn't exist + * @since 35.0.0 + */ + public function getOption(string $name): string|bool|int|float|array|null; + + /** + * Returns true if an option exists by name. + * @since 35.0.0 + */ + public function hasOption(string $name): bool; +} diff --git a/lib/public/Console/IOutput.php b/lib/public/Console/IOutput.php new file mode 100644 index 0000000000000..727a97344ab17 --- /dev/null +++ b/lib/public/Console/IOutput.php @@ -0,0 +1,95 @@ + $items + * @since 35.0.0 + */ + public function writeArrayInOutputFormat(iterable $items, string $prefix = ' - '): void; + + /** + * Write a multidimensional array of items in the format specified with --output + * + * @param list> $items + * @since 35.0.0 + */ + public function writeTableInOutputFormat(array $items): void; + + /** + * Write a multidimensional iterator of items in the format specified with --output + * + * @param \Iterator> $items + * @since 35.0.0 + */ + public function writeStreamingTableInOutputFormat(\Iterator $items, int $tableGroupSize): void; +} diff --git a/lib/public/Console/IQuestionHelper.php b/lib/public/Console/IQuestionHelper.php new file mode 100644 index 0000000000000..a2e5f5989bcef --- /dev/null +++ b/lib/public/Console/IQuestionHelper.php @@ -0,0 +1,23 @@ +