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 @@
+