diff --git a/apps/files/lib/Command/Copy.php b/apps/files/lib/Command/Copy.php
index 1d905e73e986b..fe9a00befcde1 100644
--- a/apps/files/lib/Command/Copy.php
+++ b/apps/files/lib/Command/Copy.php
@@ -9,77 +9,80 @@
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\IInput;
+use OCP\Console\IOutput;
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,
+ IInput $input,
+ #[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 +90,17 @@ 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;
+ if (!$input->confirm('' . $target . ' already exists, overwrite? [y/N] ', false)) {
+ return ExitCode::Failure;
}
}
}
@@ -110,9 +109,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..7d1e31ab4c5cb 100644
--- a/apps/files/lib/Command/Delete.php
+++ b/apps/files/lib/Command/Delete.php
@@ -11,58 +11,55 @@
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\IInput;
+use OCP\Console\IOutput;
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,
+ IInput $input,
+ #[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);
+ if ($input->confirm("$file in a shared file, do you want to unshare the file from $user instead of deleting the source file? [Y/n] ", true)) {
$storage->unshareStorage();
- return self::SUCCESS;
+ return ExitCode::Success;
} else {
$node = $storage->getShare()->getNode();
$output->writeln('');
@@ -76,8 +73,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('');
@@ -88,8 +85,7 @@ public function execute(InputInterface $input, OutputInterface $output): int {
} else {
$maybeContents = '';
}
- $question = new ConfirmationQuestion('Delete ' . $node->getPath() . $maybeContents . '? [y/N] ', false);
- $deleteConfirmed = $helper->ask($input, $output, $question);
+ $deleteConfirmed = $input->confirm('Delete ' . $node->getPath() . $maybeContents . '? [y/N] ', false);
}
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..0563584d75be2 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..40a98e519839d 100644
--- a/apps/files/lib/Command/Get.php
+++ b/apps/files/lib/Command/Get.php
@@ -9,65 +9,62 @@
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..ae17735969868 100644
--- a/apps/files/lib/Command/Mkdir.php
+++ b/apps/files/lib/Command/Mkdir.php
@@ -9,46 +9,43 @@
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..c8f6d5c8b243d 100644
--- a/apps/files/lib/Command/Mount/ListMounts.php
+++ b/apps/files/lib/Command/Mount/ListMounts.php
@@ -8,44 +8,46 @@
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\Console\OutputFormat;
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',
+ supportsOutputFormat: true,
+)]
+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,
+ OutputFormat $outputFormat,
+ #[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) {
@@ -63,9 +65,7 @@ public function execute(InputInterface $input, OutputInterface $output): int {
/** @var array $cachedByMountpoint */
$cachedByMountPoint = array_combine(array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts), $cachedMounts);
- $format = $input->getOption('output');
-
- if ($format === self::OUTPUT_FORMAT_PLAIN) {
+ if ($outputFormat === OutputFormat::Plain) {
foreach ($mounts as $mount) {
$output->writeln('' . $mount->getMountPoint() . ': ' . $mount->getStorageId());
if (isset($cachedByMountPoint[$mount->getMountPoint()])) {
@@ -101,12 +101,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..59bc0c30814e0 100644
--- a/apps/files/lib/Command/Mount/Refresh.php
+++ b/apps/files/lib/Command/Mount/Refresh.php
@@ -8,38 +8,36 @@
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 +47,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..7f8e03fa3370b 100644
--- a/apps/files/lib/Command/Move.php
+++ b/apps/files/lib/Command/Move.php
@@ -9,89 +9,84 @@
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\IInput;
+use OCP\Console\IOutput;
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,
+ IInput $input,
+ #[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;
+ if (!$input->confirm('' . $target . ' already exists, overwrite? [y/N] ', false)) {
+ return ExitCode::Failure;
}
}
}
@@ -100,9 +95,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..6d900e3b1389b 100644
--- a/apps/files/lib/Command/Object/Delete.php
+++ b/apps/files/lib/Command/Object/Delete.php
@@ -8,34 +8,32 @@
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 Symfony\Component\Console\Question\ConfirmationQuestion;
-
-class Delete extends Command {
+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;
+
+#[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,
+ IInput $input,
+ #[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 +49,9 @@ 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 ($input->confirm("Delete $object? [y/N] ", false)) {
$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..60fd99901df39 100644
--- a/apps/files/lib/Command/Object/Get.php
+++ b/apps/files/lib/Command/Object/Get.php
@@ -8,41 +8,39 @@
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 +48,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..5e99a29642326 100644
--- a/apps/files/lib/Command/Object/Info.php
+++ b/apps/files/lib/Command/Object/Info.php
@@ -8,49 +8,51 @@
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\Console\OutputFormat;
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',
+ supportsOutputFormat: true,
+)]
+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,
+ OutputFormat $outputFormat,
+ #[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,10 +60,10 @@ 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'])) {
+ if ($outputFormat === OutputFormat::Plain && isset($meta['size'])) {
$meta['size'] = Util::humanFileSize($meta['size']);
}
if (isset($meta['mtime'])) {
@@ -74,9 +76,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..f20c8d63170c9 100644
--- a/apps/files/lib/Command/Object/ListObject.php
+++ b/apps/files/lib/Command/Object/ListObject.php
@@ -8,45 +8,47 @@
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\Console\OutputFormat;
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',
+ supportsOutputFormat: true,
+)]
+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,
+ OutputFormat $outputFormat,
+ #[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, $outputFormat === OutputFormat::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..29034a49cdddf 100644
--- a/apps/files/lib/Command/Object/Multi/Rename.php
+++ b/apps/files/lib/Command/Object/Multi/Rename.php
@@ -8,54 +8,49 @@
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\IInput;
+use OCP\Console\IOutput;
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,
+ IInput $input,
+ #[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 +62,7 @@ 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 ($input->confirm('Automatically create target object store configuration? [y/N] ', false)) {
$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..a1090712eebb6 100644
--- a/apps/files/lib/Command/Object/Multi/Users.php
+++ b/apps/files/lib/Command/Object/Multi/Users.php
@@ -8,70 +8,70 @@
namespace OCA\Files\Command\Object\Multi;
-use OC\Core\Command\Base;
use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
-use OCP\IConfig;
+use OCP\Config\IUserConfig;
+use OCP\Console\Attribute\AsCommand;
+use OCP\Console\Attribute\Option;
+use OCP\Console\ExitCode;
+use OCP\Console\IOutput;
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',
+ supportsOutputFormat: true,
+)]
+class Users {
public function __construct(
private readonly IUserManager $userManager,
private readonly PrimaryObjectStoreConfig $objectStoreConfig,
- private readonly IConfig $config,
+ private readonly IUserConfig $userConfig,
) {
- 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));
+ $users = $this->getUsers($this->userConfig->searchUsersByValueString('homeobjectstore', 'bucket', $bucket));
} elseif ($bucket === '' && $objectStore !== '') {
- $users = $this->getUsers($this->config->getUsersForUserValue('homeobjectstore', 'objectstore', $objectStore));
+ $users = $this->getUsers($this->userConfig->searchUsersByValueString('homeobjectstore', 'objectstore', $objectStore));
} elseif ($bucket) {
$users = $this->getUsers(array_intersect(
- $this->config->getUsersForUserValue('homeobjectstore', 'bucket', $bucket),
- $this->config->getUsersForUserValue('homeobjectstore', 'objectstore', $objectStore)
+ iterator_to_array($this->userConfig->searchUsersByValueString('homeobjectstore', 'bucket', $bucket)),
+ iterator_to_array($this->userConfig->searchUsersByValueString('homeobjectstore', 'objectstore', $objectStore))
));
} else {
$users = $this->userManager->getSeenUsers();
}
}
- $this->writeStreamingTableInOutputFormat($input, $output, $this->infoForUsers($users), 100);
- return 0;
+ $output->writeStreamingTableInOutputFormat($this->infoForUsers($users), 100);
+ return ExitCode::Success;
}
/**
- * @param string[] $userIds
+ * @param iterable $userIds
* @return \Iterator
*/
- private function getUsers(array $userIds): \Iterator {
+ private function getUsers(iterable $userIds): \Iterator {
foreach ($userIds as $userId) {
$user = $this->userManager->get($userId);
if ($user) {
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..ae8a9af401140 100644
--- a/apps/files/lib/Command/Object/Orphans.php
+++ b/apps/files/lib/Command/Object/Orphans.php
@@ -8,15 +8,22 @@
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\Console\OutputFormat;
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',
+ supportsOutputFormat: true,
+)]
+class Orphans {
private const CHUNK_SIZE = 100;
private ?IQueryBuilder $query = null;
@@ -25,7 +32,6 @@ public function __construct(
private readonly ObjectUtil $objectUtils,
private readonly IDBConnection $connection,
) {
- parent::__construct();
}
private function getQuery(): IQueryBuilder {
@@ -38,25 +44,21 @@ 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,
+ OutputFormat $outputFormat,
+ #[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 +68,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, $outputFormat === OutputFormat::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..022eb8397f36b 100644
--- a/apps/files/lib/Command/Object/Put.php
+++ b/apps/files/lib/Command/Object/Put.php
@@ -8,63 +8,56 @@
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\IInput;
+use OCP\Console\IOutput;
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,
+ IInput $consoleInput,
+ #[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 (!$consoleInput->confirm('Write to the object anyway? [y/N] ', false)) {
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..864b607eeb378 100644
--- a/apps/files/lib/Command/Put.php
+++ b/apps/files/lib/Command/Put.php
@@ -9,66 +9,63 @@
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..32233b95db836 100644
--- a/apps/files/lib/Command/RepairTree.php
+++ b/apps/files/lib/Command/RepairTree.php
@@ -9,39 +9,37 @@
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 {
- public const CHUNK_SIZE = 200;
+#[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 int 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 +55,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 +77,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/Scan.php b/apps/files/lib/Command/Scan.php
index 8e17671e2ef97..d070a62842a72 100644
--- a/apps/files/lib/Command/Scan.php
+++ b/apps/files/lib/Command/Scan.php
@@ -1,5 +1,7 @@
setName('files:scan')
- ->setDescription('rescan filesystem')
- ->addArgument(
- 'user_id',
- InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
- 'will rescan all files of the given user(s)'
- )
- ->addOption(
- 'path',
- 'p',
- InputOption::VALUE_REQUIRED,
- 'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored'
- )
- ->addOption(
- 'generate-metadata',
- null,
- InputOption::VALUE_OPTIONAL,
- 'Generate metadata for all scanned files; if specified only generate for named value',
- ''
- )
- ->addOption(
- 'all',
- null,
- InputOption::VALUE_NONE,
- 'will rescan all files of all known users'
- )->addOption(
- 'unscanned',
- null,
- InputOption::VALUE_NONE,
- 'only scan files which are marked as not fully scanned'
- )->addOption(
- 'shallow',
- null,
- InputOption::VALUE_NONE,
- 'do not scan folders recursively'
- )->addOption(
- 'home-only',
- null,
- InputOption::VALUE_NONE,
- 'only scan the home storage, ignoring any mounted external storage or share'
- );
+ public function __invoke(
+ IOutput $output,
+ ISignalHandler $signalHandler,
+ #[Argument(name: 'user_id', description: 'will rescan all files of the given user(s)')]
+ array $userIds = [],
+ #[Option(description: 'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored', shortcut: 'p')]
+ ?string $path = null,
+ #[Option(name: 'generate-metadata', description: 'Generate metadata for all scanned files; if specified only generate for named value')]
+ string|bool $generateMetadata = false,
+ #[Option(description: 'will rescan all files of all known users')]
+ bool $all = false,
+ #[Option(description: 'only scan files which are marked as not fully scanned')]
+ bool $unscanned = false,
+ #[Option(description: 'do not scan folders recursively')]
+ bool $shallow = false,
+ #[Option(name: 'home-only', description: 'only scan the home storage, ignoring any mounted external storage or share')]
+ bool $homeOnly = false,
+ ): ExitCode {
+ $inputPath = $path;
+ if ($inputPath) {
+ $inputPath = '/' . trim($inputPath, '/');
+ [, $user,] = explode('/', $inputPath, 3);
+ $users = [$user];
+ } elseif ($all) {
+ $users = $this->userManager->search('');
+ } else {
+ $users = $userIds;
+ }
+
+ # check quantity of users to be process and show it on the command line
+ $users_total = count($users);
+ if ($users_total === 0) {
+ $output->writeln('Please specify the user id to scan, --all to scan for all users or --path=...');
+ return ExitCode::Failure;
+ }
+
+ $this->initTools($output);
+
+ // null if --generate-metadata is not set, empty if option has no value, value if set
+ $metadata = match (true) {
+ $generateMetadata === false => null,
+ $generateMetadata === true => '',
+ default => $generateMetadata,
+ };
+
+ $scannedStorages = [];
+ $mountFilter = function (IMountPoint $mount) use ($homeOnly, &$scannedStorages) {
+ if ($homeOnly && !$this->isHomeMount($mount)) {
+ return false;
+ }
+
+ // when scanning multiple users, the scanner might encounter the same storage multiple times (e.g. external storages, or group folders)
+ // we can filter out any storage we've already scanned to avoid double work
+ $storage = $mount->getStorage();
+ $storageKey = $storage->getId();
+ while ($storage->instanceOfStorage(Jail::class)) {
+ $storageKey .= '/' . $storage->getUnjailedPath('');
+ $storage = $storage->getUnjailedStorage();
+ }
+ if (array_key_exists($storageKey, $scannedStorages)) {
+ return false;
+ }
+
+ $scannedStorages[$storageKey] = true;
+ return true;
+ };
+
+ $user_count = 0;
+ foreach ($users as $user) {
+ if (is_object($user)) {
+ $user = $user->getUID();
+ }
+ $scanPath = $inputPath ?: '/' . $user;
+ ++$user_count;
+ if ($this->userManager->userExists($user)) {
+ $output->writeln("Starting scan for user $user_count out of $users_total ($user)");
+ $this->scanFiles(
+ $user,
+ $scanPath,
+ $metadata,
+ $output,
+ $signalHandler,
+ $mountFilter,
+ $unscanned,
+ !$shallow,
+ );
+ $output->writeln('', Verbosity::Verbose);
+ } else {
+ $output->writeln("Unknown user $user_count $user");
+ $output->writeln('', Verbosity::Verbose);
+ }
+
+ try {
+ $signalHandler->abortIfInterrupted();
+ } catch (InterruptedException) {
+ break;
+ }
+ }
+
+ $this->presentStats($output);
+ return ExitCode::Success;
}
protected function scanFiles(
string $user,
string $path,
?string $scanMetadata,
- OutputInterface $output,
+ IOutput $output,
+ ISignalHandler $signalHandler,
callable $mountFilter,
bool $backgroundScan = false,
bool $recursive = true,
@@ -123,10 +186,10 @@ protected function scanFiles(
);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata): void {
- $output->writeln("\tFile\t$path", OutputInterface::VERBOSITY_VERBOSE);
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $signalHandler, $scanMetadata): void {
+ $output->writeln("\tFile\t$path", Verbosity::Verbose);
++$this->filesCounter;
- $this->abortIfInterrupted();
+ $signalHandler->abortIfInterrupted();
if ($scanMetadata !== null) {
$node = $this->rootFolder->get($path);
$this->filesMetadataManager->refreshMetadata(
@@ -137,14 +200,14 @@ protected function scanFiles(
}
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output): void {
- $output->writeln("\tFolder\t$path", OutputInterface::VERBOSITY_VERBOSE);
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output, $signalHandler): void {
+ $output->writeln("\tFolder\t$path", Verbosity::Verbose);
++$this->foldersCounter;
- $this->abortIfInterrupted();
+ $signalHandler->abortIfInterrupted();
});
$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output): void {
- $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
+ $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', Verbosity::Verbose);
++$this->errorsCounter;
});
@@ -174,7 +237,7 @@ protected function scanFiles(
$output->writeln(' ' . $e->getMessage());
$output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
++$this->errorsCounter;
- } catch (InterruptedException $e) {
+ } catch (InterruptedException) {
# exit the function if ctrl-c has been pressed
$output->writeln('Interrupted by user');
} catch (NotFoundException $e) {
@@ -198,96 +261,10 @@ public function isHomeMount(IMountPoint $mountPoint): bool {
return substr_count($mountPoint->getMountPoint(), '/') <= 3;
}
- #[\Override]
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $inputPath = $input->getOption('path');
- if ($inputPath) {
- $inputPath = '/' . trim($inputPath, '/');
- [, $user,] = explode('/', $inputPath, 3);
- $users = [$user];
- } elseif ($input->getOption('all')) {
- $users = $this->userManager->search('');
- } else {
- $users = $input->getArgument('user_id');
- }
-
- # check quantity of users to be process and show it on the command line
- $users_total = count($users);
- if ($users_total === 0) {
- $output->writeln('Please specify the user id to scan, --all to scan for all users or --path=...');
- return self::FAILURE;
- }
-
- $this->initTools($output);
-
- // getOption() logic on VALUE_OPTIONAL
- $metadata = null; // null if --generate-metadata is not set, empty if option have no value, value if set
- if ($input->getOption('generate-metadata') !== '') {
- $metadata = $input->getOption('generate-metadata') ?? '';
- }
-
- $homeOnly = $input->getOption('home-only');
- $scannedStorages = [];
- $mountFilter = function (IMountPoint $mount) use ($homeOnly, &$scannedStorages) {
- if ($homeOnly && !$this->isHomeMount($mount)) {
- return false;
- }
-
- // when scanning multiple users, the scanner might encounter the same storage multiple times (e.g. external storages, or group folders)
- // we can filter out any storage we've already scanned to avoid double work
- $storage = $mount->getStorage();
- $storageKey = $storage->getId();
- while ($storage->instanceOfStorage(Jail::class)) {
- $storageKey .= '/' . $storage->getUnjailedPath('');
- $storage = $storage->getUnjailedStorage();
- }
- if (array_key_exists($storageKey, $scannedStorages)) {
- return false;
- }
-
- $scannedStorages[$storageKey] = true;
- return true;
- };
-
- $user_count = 0;
- foreach ($users as $user) {
- if (is_object($user)) {
- $user = $user->getUID();
- }
- $path = $inputPath ?: '/' . $user;
- ++$user_count;
- if ($this->userManager->userExists($user)) {
- $output->writeln("Starting scan for user $user_count out of $users_total ($user)");
- $this->scanFiles(
- $user,
- $path,
- $metadata,
- $output,
- $mountFilter,
- $input->getOption('unscanned'),
- !$input->getOption('shallow'),
- );
- $output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
- } else {
- $output->writeln("Unknown user $user_count $user");
- $output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
- }
-
- try {
- $this->abortIfInterrupted();
- } catch (InterruptedException $e) {
- break;
- }
- }
-
- $this->presentStats($output);
- return self::SUCCESS;
- }
-
/**
* Initialises some useful tools for the Command
*/
- protected function initTools(OutputInterface $output): void {
+ protected function initTools(IOutput $output): void {
// Start the timer
$this->execTime = -microtime(true);
// Convert PHP errors to exceptions
@@ -308,48 +285,35 @@ protected function initTools(OutputInterface $output): void {
* @param string $file the filename that the error was raised in
* @param int $line the line number the error was raised
*/
- public function exceptionErrorHandler(OutputInterface $output, int $severity, string $message, string $file, int $line): bool {
+ public function exceptionErrorHandler(IOutput $output, int $severity, string $message, string $file, int $line): bool {
if (($severity === E_DEPRECATED) || ($severity === E_USER_DEPRECATED)) {
// Do not show deprecation warnings
return false;
}
$e = new \ErrorException($message, 0, $severity, $file, $line);
$output->writeln('Error during scan: ' . $e->getMessage() . '');
- $output->writeln('' . $e->getTraceAsString() . '', OutputInterface::VERBOSITY_VERY_VERBOSE);
+ $output->writeln('' . $e->getTraceAsString() . '', Verbosity::VeryVerbose);
++$this->errorsCounter;
return true;
}
- protected function presentStats(OutputInterface $output): void {
+ protected function presentStats(IOutput $output): void {
// Stop the timer
$this->execTime += microtime(true);
$this->logger->info("Completed scan of {$this->filesCounter} files in {$this->foldersCounter} folder. Found {$this->newCounter} new, {$this->updatedCounter} updated and {$this->removedCounter} removed items");
- $headers = [
- 'Folders',
- 'Files',
- 'New',
- 'Updated',
- 'Removed',
- 'Errors',
- 'Elapsed time',
+ $row = [
+ 'Folders' => $this->foldersCounter,
+ 'Files' => $this->filesCounter,
+ 'New' => $this->newCounter,
+ 'Updated' => $this->updatedCounter,
+ 'Removed' => $this->removedCounter,
+ 'Errors' => $this->errorsCounter,
+ 'Elapsed time' => $this->formatExecTime(),
];
- $niceDate = $this->formatExecTime();
- $rows = [
- $this->foldersCounter,
- $this->filesCounter,
- $this->newCounter,
- $this->updatedCounter,
- $this->removedCounter,
- $this->errorsCounter,
- $niceDate,
- ];
- $table = new Table($output);
- $table
- ->setHeaders($headers)
- ->setRows([$rows]);
- $table->render();
+
+ $output->writeTableInOutputFormat([$row]);
}
/**
@@ -361,7 +325,7 @@ protected function formatExecTime(): string {
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), $secs % 60);
}
- protected function reconnectToDatabase(OutputInterface $output): Connection {
+ protected function reconnectToDatabase(IOutput $output): Connection {
/** @var Connection $connection */
$connection = Server::get(Connection::class);
try {
diff --git a/apps/files/lib/Command/ScanAppData.php b/apps/files/lib/Command/ScanAppData.php
index eab0237486c53..adf424c3600cd 100644
--- a/apps/files/lib/Command/ScanAppData.php
+++ b/apps/files/lib/Command/ScanAppData.php
@@ -1,5 +1,7 @@
getVerbosity()->value > Verbosity::Verbose->value) {
+ $output->setVerbosity(Verbosity::Verbose);
+ }
+
+ $output->writeln('Scanning AppData for files');
+ $output->writeln('');
+
+ // Start the timer
+ $this->execTime = -microtime(true);
- $this
- ->setName('files:scan-app-data')
- ->setDescription('rescan the AppData folder');
+ $this->initTools();
- $this->addArgument('folder', InputArgument::OPTIONAL, 'The appdata subfolder to scan', '');
+ $exitCode = $this->scanFiles($output, $signalHandler, $folder);
+ if ($exitCode === ExitCode::Success) {
+ $this->presentStats($output);
+ }
+ return $exitCode;
}
- protected function getScanner(OutputInterface $output): Scanner {
+ protected function getScanner(IOutput $output): Scanner {
$connection = $this->reconnectToDatabase($output);
return new Scanner(
null,
new ConnectionAdapter($connection),
- Server::get(IEventDispatcher::class),
- Server::get(LoggerInterface::class),
- Server::get(SetupManager::class),
+ $this->eventDispatcher,
+ $this->logger,
+ $this->setupManager,
);
}
- protected function scanFiles(OutputInterface $output, string $folder): int {
+ protected function scanFiles(IOutput $output, ISignalHandler $signalHandler, string $folder): ExitCode {
if ($folder === 'preview' || $folder === '') {
$this->previewsCounter = $this->previewStorage->scan();
if ($folder === 'preview') {
- return self::SUCCESS;
+ return ExitCode::Success;
}
}
@@ -81,7 +106,7 @@ protected function scanFiles(OutputInterface $output, string $folder): int {
$appData = $this->getAppDataFolder();
} catch (NotFoundException $e) {
$output->writeln('NoAppData folder found');
- return self::FAILURE;
+ return ExitCode::Failure;
}
if ($folder !== '') {
@@ -89,27 +114,27 @@ protected function scanFiles(OutputInterface $output, string $folder): int {
$appData = $appData->get($folder);
} catch (NotFoundException $e) {
$output->writeln('Could not find folder: ' . $folder . '');
- return self::FAILURE;
+ return ExitCode::Failure;
}
}
$scanner = $this->getScanner($output);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output): void {
- $output->writeln("\tFile $path", OutputInterface::VERBOSITY_VERBOSE);
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output, $signalHandler): void {
+ $output->writeln("\tFile $path", Verbosity::Verbose);
++$this->filesCounter;
- $this->abortIfInterrupted();
+ $signalHandler->abortIfInterrupted();
});
- $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output): void {
- $output->writeln("\tFolder $path", OutputInterface::VERBOSITY_VERBOSE);
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output, $signalHandler): void {
+ $output->writeln("\tFolder $path", Verbosity::Verbose);
++$this->foldersCounter;
- $this->abortIfInterrupted();
+ $signalHandler->abortIfInterrupted();
});
$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output): void {
- $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
+ $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', Verbosity::Verbose);
});
$scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output): void {
@@ -121,45 +146,21 @@ protected function scanFiles(OutputInterface $output, string $folder): int {
} catch (ForbiddenException $e) {
$output->writeln('Storage not writable');
$output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
- return self::FAILURE;
+ return ExitCode::Failure;
} catch (InterruptedException $e) {
# exit the function if ctrl-c has been pressed
$output->writeln('Interrupted by user');
- return self::FAILURE;
+ return ExitCode::Failure;
} catch (NotFoundException $e) {
$output->writeln('Path not found: ' . $e->getMessage() . '');
- return self::FAILURE;
+ return ExitCode::Failure;
} catch (\Exception $e) {
$output->writeln('Exception during scan: ' . $e->getMessage() . '');
$output->writeln('' . $e->getTraceAsString() . '');
- return self::FAILURE;
- }
-
- return self::SUCCESS;
- }
-
- #[\Override]
- protected function execute(InputInterface $input, OutputInterface $output): int {
- # restrict the verbosity level to VERBOSITY_VERBOSE
- if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
- $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
+ return ExitCode::Failure;
}
- $output->writeln('Scanning AppData for files');
- $output->writeln('');
-
- $folder = $input->getArgument('folder');
-
- // Start the timer
- $this->execTime = -microtime(true);
-
- $this->initTools();
-
- $exitCode = $this->scanFiles($output, $folder);
- if ($exitCode === self::SUCCESS) {
- $this->presentStats($output);
- }
- return $exitCode;
+ return ExitCode::Success;
}
/**
@@ -190,45 +191,18 @@ public function exceptionErrorHandler(int $severity, string $message, string $fi
throw new \ErrorException($message, 0, $severity, $file, $line);
}
- protected function presentStats(OutputInterface $output): void {
+ protected function presentStats(IOutput $output): void {
// Stop the timer
$this->execTime += microtime(true);
+ $row = [];
if ($this->previewsCounter !== -1) {
- $headers[] = 'Previews';
+ $row['Previews'] = $this->previewsCounter;
}
- $headers[] = 'Folders';
- $headers[] = 'Files';
- $headers[] = 'Elapsed time';
-
- $this->showSummary($headers, null, $output);
- }
-
- /**
- * Shows a summary of operations
- *
- * @param string[] $headers
- * @param string[] $rows
- */
- protected function showSummary(array $headers, ?array $rows, OutputInterface $output): void {
- $niceDate = $this->formatExecTime();
- if (!$rows) {
- if ($this->previewsCounter !== -1) {
- $rows[] = $this->previewsCounter;
- }
- $rows[] = $this->foldersCounter;
- $rows[] = $this->filesCounter;
- $rows[] = $niceDate;
- }
-
- $this->displayTable($output, $headers, $rows);
- }
+ $row['Folders'] = $this->foldersCounter;
+ $row['Files'] = $this->filesCounter;
+ $row['Elapsed time'] = $this->formatExecTime();
- protected function displayTable($output, $headers, $rows): void {
- $table = new Table($output);
- $table
- ->setHeaders($headers)
- ->setRows([$rows]);
- $table->render();
+ $output->writeTableInOutputFormat([$row]);
}
/**
@@ -240,7 +214,7 @@ protected function formatExecTime(): string {
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), (int)$secs % 60);
}
- protected function reconnectToDatabase(OutputInterface $output): Connection {
+ protected function reconnectToDatabase(IOutput $output): Connection {
/** @var Connection $connection */
$connection = Server::get(Connection::class);
try {
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/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php
index c38cd9af4f37b..c6d30e6e7ee16 100644
--- a/apps/files/lib/Command/TransferOwnership.php
+++ b/apps/files/lib/Command/TransferOwnership.php
@@ -12,109 +12,75 @@
use OCA\Files\Exception\TransferOwnershipException;
use OCA\Files\Service\OwnershipTransferService;
use OCA\Files_External\Config\ConfigAdapter;
+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\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
-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 TransferOwnership extends Command {
+#[AsCommand(
+ name: 'files:transfer-ownership',
+ description: 'All files and folders are moved to another user - outgoing shares and incoming user file shares (optionally) are moved as well.',
+)]
+class TransferOwnership {
public function __construct(
private IUserManager $userManager,
private OwnershipTransferService $transferService,
private IConfig $config,
private IMountManager $mountManager,
) {
- parent::__construct();
}
- #[\Override]
- protected function configure(): void {
- $this
- ->setName('files:transfer-ownership')
- ->setDescription('All files and folders are moved to another user - outgoing shares and incoming user file shares (optionally) are moved as well.')
- ->addArgument(
- 'source-user',
- InputArgument::REQUIRED,
- 'owner of files which shall be moved'
- )
- ->addArgument(
- 'destination-user',
- InputArgument::REQUIRED,
- 'user who will be the new owner of the files'
- )
- ->addOption(
- 'path',
- null,
- InputOption::VALUE_REQUIRED,
- 'selectively provide the path to transfer. For example --path="folder_name"',
- ''
- )->addOption(
- 'move',
- null,
- InputOption::VALUE_NONE,
- 'move data from source user to root directory of destination user, which must be empty'
- )->addOption(
- 'transfer-incoming-shares',
- null,
- InputOption::VALUE_OPTIONAL,
- 'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore',
- '2'
- )->addOption(
- 'include-external-storage',
- null,
- InputOption::VALUE_NONE,
- 'include files on external storages, this will _not_ setup an external storage for the target user, but instead moves all the files from the external storages into the target users home directory',
- )->addOption(
- 'force-include-external-storage',
- null,
- InputOption::VALUE_NONE,
- 'don\'t ask for confirmation for transferring external storages',
- )
- ->addOption(
- 'use-user-id',
- null,
- InputOption::VALUE_NONE,
- 'use user ID instead of display name in the transferred folder name',
- );
- }
-
- #[\Override]
- protected function execute(InputInterface $input, OutputInterface $output): int {
-
+ public function __invoke(
+ IOutput $output,
+ IInput $input,
+ #[Argument(name: 'source-user', description: 'owner of files which shall be moved')]
+ string $sourceUser,
+ #[Argument(name: 'destination-user', description: 'user who will be the new owner of the files')]
+ string $destinationUser,
+ #[Option(description: 'selectively provide the path to transfer. For example --path="folder_name"')]
+ string $path = '',
+ #[Option(description: 'move data from source user to root directory of destination user, which must be empty')]
+ bool $move = false,
+ #[Option(name: 'transfer-incoming-shares', description: 'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore')]
+ string|bool $transferIncomingShares = false,
+ #[Option(name: 'include-external-storage', description: 'include files on external storages, this will _not_ setup an external storage for the target user, but instead moves all the files from the external storages into the target users home directory')]
+ bool $includeExternalStorage = false,
+ #[Option(name: 'force-include-external-storage', description: "don't ask for confirmation for transferring external storages")]
+ bool $forceIncludeExternalStorage = false,
+ #[Option(name: 'use-user-id', description: 'use user ID instead of display name in the transferred folder name')]
+ bool $useUserId = false,
+ ): ExitCode {
/**
* Check if source and destination users are same. If they are same then just ignore the transfer.
*/
-
- if ($input->getArgument(('source-user')) === $input->getArgument('destination-user')) {
+ if ($sourceUser === $destinationUser) {
$output->writeln("Ownership can't be transferred when Source and Destination users are the same user. Please check your input.");
- return self::FAILURE;
+ return ExitCode::Failure;
}
- $sourceUserObject = $this->userManager->get($input->getArgument('source-user'));
- $destinationUserObject = $this->userManager->get($input->getArgument('destination-user'));
+ $sourceUserObject = $this->userManager->get($sourceUser);
+ $destinationUserObject = $this->userManager->get($destinationUser);
if (!$sourceUserObject instanceof IUser) {
- $output->writeln('Unknown source user ' . $input->getArgument('source-user') . '');
- return self::FAILURE;
+ $output->writeln('Unknown source user ' . $sourceUser . '');
+ return ExitCode::Failure;
}
if (!$destinationUserObject instanceof IUser) {
- $output->writeln('Unknown destination user ' . $input->getArgument('destination-user') . '');
- return self::FAILURE;
+ $output->writeln('Unknown destination user ' . $destinationUser . '');
+ return ExitCode::Failure;
}
- $path = ltrim($input->getOption('path'), '/');
- $includeExternalStorage = $input->getOption('include-external-storage');
+ $normalizedPath = ltrim($path, '/');
if ($includeExternalStorage) {
- $mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/'));
+ $mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $normalizedPath, '/'));
/** @var IMountPoint[] $mounts */
$mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class);
if (count($mounts) > 0) {
@@ -125,12 +91,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('');
$output->writeln('Any other users with access to these external storages will lose access to the files.');
$output->writeln('');
- if (!$input->getOption('force-include-external-storage')) {
- /** @var QuestionHelper $helper */
- $helper = $this->getHelper('question');
- $question = new ConfirmationQuestion('Are you sure you want to transfer external storages? (y/N) ', false);
- if (!$helper->ask($input, $output, $question)) {
- return self::FAILURE;
+ if (!$forceIncludeExternalStorage) {
+ if (!$input->confirm('Are you sure you want to transfer external storages? (y/N) ', false)) {
+ return ExitCode::Failure;
}
}
}
@@ -140,18 +103,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->transferService->transfer(
$sourceUserObject,
$destinationUserObject,
- $path,
+ $normalizedPath,
$output,
- $input->getOption('move') === true,
+ $move,
false,
$includeExternalStorage,
- $input->getOption('use-user-id') === true,
+ $useUserId,
);
} catch (TransferOwnershipException $e) {
$output->writeln('' . $e->getMessage() . '');
- return $e->getCode() !== 0 ? $e->getCode() : self::FAILURE;
+ $exitCode = $e->getCode() !== 0 ? ExitCode::tryFrom($e->getCode()) : null;
+ return $exitCode ?? ExitCode::Failure;
}
- return self::SUCCESS;
+ return ExitCode::Success;
}
}
diff --git a/apps/files/lib/Command/WindowsCompatibleFilenames.php b/apps/files/lib/Command/WindowsCompatibleFilenames.php
index d595d5731b68b..5de9b13ed1b15 100644
--- a/apps/files/lib/Command/WindowsCompatibleFilenames.php
+++ b/apps/files/lib/Command/WindowsCompatibleFilenames.php
@@ -9,47 +9,45 @@
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/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php
index 5cfff142a526f..63045e79a826f 100644
--- a/apps/files/lib/Service/OwnershipTransferService.php
+++ b/apps/files/lib/Service/OwnershipTransferService.php
@@ -11,12 +11,15 @@
use Closure;
use Exception;
+use OC\Console\NullOutput;
use OC\Files\Filesystem;
use OC\Files\View;
use OCA\Encryption\Util;
use OCA\Files\Exception\TransferOwnershipException;
use OCA\Files_External\Config\ConfigAdapter;
use OCA\GroupFolders\Mount\GroupMountPoint;
+use OCP\Console\IOutput;
+use OCP\Console\Verbosity;
use OCP\Encryption\IManager as IEncryptionManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\IHomeMountProvider;
@@ -34,9 +37,6 @@
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
use OCP\User\Exceptions\UserNotFoundException;
-use Symfony\Component\Console\Helper\ProgressBar;
-use Symfony\Component\Console\Output\NullOutput;
-use Symfony\Component\Console\Output\OutputInterface;
use function array_merge;
use function basename;
use function count;
@@ -63,7 +63,7 @@ public function __construct(
* @param IUser $destinationUser
* @param string $path
*
- * @param OutputInterface|null $output
+ * @param IOutput|null $output
* @param bool $move
* @throws TransferOwnershipException
* @throws UserNotFoundException
@@ -72,7 +72,7 @@ public function transfer(
IUser $sourceUser,
IUser $destinationUser,
string $path,
- ?OutputInterface $output = null,
+ ?IOutput $output = null,
bool $move = false,
bool $firstLogin = false,
bool $includeExternalStorage = false,
@@ -245,7 +245,7 @@ private function walkFiles(View $view, $path, Closure $callBack) {
}
/**
- * @param OutputInterface $output
+ * @param IOutput $output
*
* @throws TransferOwnershipException
*/
@@ -254,7 +254,7 @@ protected function analyse(
string $destinationUid,
string $sourcePath,
View $view,
- OutputInterface $output,
+ IOutput $output,
bool $includeExternalStorage = false,
): void {
$output->writeln('Validating quota');
@@ -269,8 +269,7 @@ protected function analyse(
}
$output->writeln("Analysing files of $sourceUid ...");
- $progress = new ProgressBar($output);
- $progress->start();
+ $output->progressStart();
if ($this->encryptionManager->isEnabled()) {
$masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled();
@@ -284,7 +283,7 @@ protected function analyse(
$encryptedFiles[] = $sourceFileInfo;
} else {
$this->walkFiles($view, $sourcePath,
- function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
+ function (FileInfo $fileInfo) use ($output, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
$mount = $fileInfo->getMountPoint();
// only analyze into folders from main storage,
@@ -301,7 +300,7 @@ function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFile
return false;
}
}
- $progress->advance();
+ $output->progressAdvance();
if ($fileInfo->isEncrypted() && !$masterKeyEnabled) {
/* Encrypted file means SSE, we can only transfer it if master key is enabled */
$encryptedFiles[] = $fileInfo;
@@ -313,7 +312,7 @@ function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFile
/* Encrypted file means SSE, we can only transfer it if master key is enabled */
$encryptedFiles[] = $sourceFileInfo;
}
- $progress->finish();
+ $output->progressFinish();
$output->writeln('');
// no file is allowed to be encrypted
@@ -332,14 +331,14 @@ function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFile
*/
private function collectUsersShares(
string $sourceUid,
- OutputInterface $output,
+ IOutput $output,
View $view,
string $path,
): array {
$output->writeln("Collecting all share information for files and folders of $sourceUid ...");
$shares = [];
- $progress = new ProgressBar($output);
+ $output->progressStart();
$normalizedPath = Filesystem::normalizePath($path);
@@ -358,7 +357,7 @@ private function collectUsersShares(
$offset = 0;
while (true) {
$sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false);
- $progress->advance(count($sharePage));
+ $output->progressAdvance(count($sharePage));
if (empty($sharePage)) {
break;
}
@@ -379,7 +378,7 @@ private function collectUsersShares(
}
}
- $progress->finish();
+ $output->progressFinish();
$output->writeln('');
return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
@@ -399,19 +398,19 @@ private function collectUsersShares(
private function collectIncomingShares(
string $sourceUid,
- OutputInterface $output,
+ IOutput $output,
?string $path,
): array {
$output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");
$shares = [];
- $progress = new ProgressBar($output);
+ $output->progressStart();
$normalizedPath = Filesystem::normalizePath($path);
$offset = 0;
while (true) {
$sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
- $progress->advance(count($sharePage));
+ $output->progressAdvance(count($sharePage));
if (empty($sharePage)) {
break;
}
@@ -433,7 +432,7 @@ private function collectIncomingShares(
$offset += 50;
}
- $progress->finish();
+ $output->progressFinish();
$output->writeln('');
return $shares;
}
@@ -446,7 +445,7 @@ protected function transferFiles(
string $sourcePath,
string $finalTarget,
View $view,
- OutputInterface $output,
+ IOutput $output,
bool $includeExternalStorage,
): void {
$output->writeln("Transferring files to $finalTarget ...");
@@ -510,14 +509,14 @@ private function restoreShares(
string $destinationUid,
string $targetLocation,
array $shares,
- OutputInterface $output,
+ IOutput $output,
):void {
$output->writeln('Restoring shares ...');
- $progress = new ProgressBar($output, count($shares));
+ $output->progressStart(count($shares));
foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
try {
- $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
+ $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), Verbosity::Verbose);
if ($share->getShareType() === IShare::TYPE_USER
&& $share->getSharedWith() === $destinationUid) {
// Unmount the shares before deleting, so we don't try to get the storage later on.
@@ -555,7 +554,7 @@ private function restoreShares(
// Try to get the new ID from the target path and suffix of the share
$node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
$newNodeId = $node->getId();
- $output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
+ $output->writeln('Had to change node id to ' . $newNodeId, Verbosity::VeryVerbose);
}
$share->setNodeId($newNodeId);
@@ -568,9 +567,9 @@ private function restoreShares(
$output->writeln('Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '');
}
$this->eventDispatcher->dispatchTyped(new ShareTransferredEvent($share));
- $progress->advance();
+ $output->progressAdvance();
}
- $progress->finish();
+ $output->progressFinish();
$output->writeln('');
}
@@ -579,13 +578,13 @@ private function transferIncomingShares(
string $destinationUid,
array $sourceShares,
array $destinationShares,
- OutputInterface $output,
+ IOutput $output,
string $path,
string $finalTarget,
bool $move,
): void {
$output->writeln('Restoring incoming shares ...');
- $progress = new ProgressBar($output, count($sourceShares));
+ $output->progressStart(count($sourceShares));
$prefix = "$destinationUid/files";
$finalShareTarget = '';
if (str_starts_with($finalTarget, $prefix)) {
@@ -618,7 +617,7 @@ private function transferIncomingShares(
$share->setNodeId($share->getNode()->getId());
$this->shareManager->updateShare($share);
// The share is already transferred.
- $progress->advance();
+ $output->progressAdvance();
if ($move) {
continue;
}
@@ -640,7 +639,7 @@ private function transferIncomingShares(
// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
$this->userMountCache->clear();
// The share is already transferred.
- $progress->advance();
+ $output->progressAdvance();
if ($move) {
continue;
}
@@ -656,9 +655,9 @@ private function transferIncomingShares(
} catch (\Throwable $e) {
$output->writeln('Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '');
}
- $progress->advance();
+ $output->progressAdvance();
}
- $progress->finish();
+ $output->progressFinish();
$output->writeln('');
}
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/apps/files/tests/Command/ScanAppDataTest.php b/apps/files/tests/Command/ScanAppDataTest.php
index 04e55804fbd73..b2abed3263613 100644
--- a/apps/files/tests/Command/ScanAppDataTest.php
+++ b/apps/files/tests/Command/ScanAppDataTest.php
@@ -11,12 +11,18 @@
namespace OCA\Files\Tests\Command;
use OC\Files\Mount\ObjectHomeMountProvider;
+use OC\Files\SetupManager;
use OC\Files\Utils\Scanner;
use OC\Preview\Db\Preview;
use OC\Preview\Db\PreviewMapper;
use OC\Preview\PreviewService;
use OC\Preview\Storage\StorageFactory;
use OCA\Files\Command\ScanAppData;
+use OCP\Console\ExitCode;
+use OCP\Console\IOutput;
+use OCP\Console\ISignalHandler;
+use OCP\Console\Verbosity;
+use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IMimeTypeLoader;
@@ -33,8 +39,7 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
+use Psr\Log\LoggerInterface;
use Test\TestCase;
#[Group(name: 'DB')]
@@ -42,8 +47,8 @@ class ScanAppDataTest extends TestCase {
private IRootFolder $rootFolder;
private IConfig $config;
private StorageFactory $storageFactory;
- private OutputInterface&MockObject $output;
- private InputInterface&MockObject $input;
+ private IOutput&MockObject $output;
+ private ISignalHandler&MockObject $signalHandler;
private Scanner&MockObject $internalScanner;
private ScanAppData $scanner;
private string $user;
@@ -56,11 +61,19 @@ public function setUp(): void {
$user = Server::get(IUserManager::class)->createUser($this->user, 'test');
Server::get(ISetupManager::class)->setupForUser($user);
Server::get(IUserSession::class)->setUser($user);
- $this->output = $this->createMock(OutputInterface::class);
- $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(IOutput::class);
+ $this->output->method('getVerbosity')->willReturn(Verbosity::Normal);
+ $this->signalHandler = $this->createMock(ISignalHandler::class);
$this->scanner = $this->getMockBuilder(ScanAppData::class)
- ->onlyMethods(['displayTable', 'initTools', 'getScanner'])
- ->setConstructorArgs([$this->rootFolder, $this->config, $this->storageFactory])
+ ->onlyMethods(['initTools', 'getScanner'])
+ ->setConstructorArgs([
+ $this->rootFolder,
+ $this->config,
+ $this->storageFactory,
+ Server::get(IEventDispatcher::class),
+ Server::get(LoggerInterface::class),
+ Server::get(SetupManager::class),
+ ])
->getMock();
$this->internalScanner = $this->getMockBuilder(Scanner::class)
->onlyMethods(['scan'])
@@ -103,23 +116,22 @@ public function testScanAppDataRoot(): void {
$this->markTestSkipped();
}
- $this->input->method('getArgument')->with('folder')->willReturn('');
$this->internalScanner->method('scan')->willReturnCallback(function (): void {
$this->internalScanner->emit('\OC\Files\Utils\Scanner', 'scanFile', ['path42']);
$this->internalScanner->emit('\OC\Files\Utils\Scanner', 'scanFolder', ['path42']);
$this->internalScanner->emit('\OC\Files\Utils\Scanner', 'scanFolder', ['path42']);
});
- $this->scanner->expects($this->once())->method('displayTable')
- ->willReturnCallback(function (OutputInterface $output, array $headers, array $rows): void {
- $this->assertEquals($this->output, $output);
- $this->assertEquals(['Previews', 'Folders', 'Files', 'Elapsed time'], $headers);
- $this->assertEquals(0, $rows[0]);
- $this->assertEquals(2, $rows[1]);
- $this->assertEquals(1, $rows[2]);
+ $this->output->expects($this->once())->method('writeTableInOutputFormat')
+ ->willReturnCallback(function (array $items): void {
+ $this->assertCount(1, $items);
+ $row = $items[0];
+ $this->assertEquals(0, $row['Previews']);
+ $this->assertEquals(2, $row['Folders']);
+ $this->assertEquals(1, $row['Files']);
});
- $errorCode = $this->invokePrivate($this->scanner, 'execute', [$this->input, $this->output]);
- $this->assertEquals(ScanAppData::SUCCESS, $errorCode);
+ $exitCode = ($this->scanner)($this->output, $this->signalHandler, '');
+ $this->assertEquals(ExitCode::Success, $exitCode);
}
public static function scanPreviewLocalData(): \Generator {
@@ -137,7 +149,6 @@ public function testScanAppDataPreviewOnlyLocalFile(bool $migrationDone, ?bool $
if ($homeProvider->getHomeMountForUser($user, $this->createMock(IStorageFactory::class)) !== null) {
$this->markTestSkipped();
}
- $this->input->method('getArgument')->with('folder')->willReturn('preview');
$file = $this->rootFolder->getUserFolder($this->user)->newFile('myfile.jpeg');
@@ -213,16 +224,16 @@ public function testScanAppDataPreviewOnlyLocalFile(bool $migrationDone, ?bool $
$mimetypeLoader = $this->createMock(IMimeTypeLoader::class);
$mimetypeLoader->method('getMimetypeById')->willReturn('image/jpeg');
- $this->scanner->expects($this->once())->method('displayTable')
- ->willReturnCallback(function ($output, array $headers, array $rows): void {
- $this->assertEquals($output, $this->output);
- $this->assertEquals(['Previews', 'Folders', 'Files', 'Elapsed time'], $headers);
- $this->assertEquals(3, $rows[0]);
- $this->assertEquals(0, $rows[1]);
- $this->assertEquals(0, $rows[2]);
+ $this->output->expects($this->once())->method('writeTableInOutputFormat')
+ ->willReturnCallback(function (array $items): void {
+ $this->assertCount(1, $items);
+ $row = $items[0];
+ $this->assertEquals(3, $row['Previews']);
+ $this->assertEquals(0, $row['Folders']);
+ $this->assertEquals(0, $row['Files']);
});
- $errorCode = $this->invokePrivate($this->scanner, 'execute', [$this->input, $this->output]);
- $this->assertEquals(ScanAppData::SUCCESS, $errorCode);
+ $exitCode = ($this->scanner)($this->output, $this->signalHandler, 'preview');
+ $this->assertEquals(ExitCode::Success, $exitCode);
/** @var Folder $previewFolder */
$previewFolder = $this->rootFolder->get($this->rootFolder->getAppDataDirectoryName() . '/preview');
diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php
index 22b244e4a7ec1..b2e22f1b007ca 100644
--- a/apps/user_ldap/lib/Configuration.php
+++ b/apps/user_ldap/lib/Configuration.php
@@ -235,7 +235,9 @@ public function setConfiguration(array $config, ?array &$applied = null): void {
$setMethod = 'setValue';
switch ($key) {
+ case 'ldapAgentName':
case 'ldapAgentPassword':
+ $val = \filter_var($val, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
$setMethod = 'setRawValue';
break;
case 'homeFolderNamingRule':
diff --git a/apps/user_ldap/tests/ConfigurationTest.php b/apps/user_ldap/tests/ConfigurationTest.php
index 5d297aae32b98..4e0270882eb09 100644
--- a/apps/user_ldap/tests/ConfigurationTest.php
+++ b/apps/user_ldap/tests/ConfigurationTest.php
@@ -55,6 +55,9 @@ public static function configurationDataProvider(): array {
$password = ' such a passw0rd ';
+ $dnWithCrlf = "cn=admin\r\nset foo bar\r\n,dc=example,dc=org";
+ $expectedDn = 'cn=adminset foo bar,dc=example,dc=org';
+
return [
'set general base' => ['ldapBase', $inputWithDN, $expectWithDN],
'set user base' => ['ldapBaseUsers', $inputWithDN, $expectWithDN],
@@ -70,6 +73,7 @@ public static function configurationDataProvider(): array {
'set login filter attributes' => ['ldapLoginFilterAttributes', $inputNames, $expectedNames],
'set agent password' => ['ldapAgentPassword', $password, $password],
+ 'set agent name strips CRLF' => ['ldapAgentName', $dnWithCrlf, $expectedDn],
'set home folder, variant 1' => ['homeFolderNamingRule', $inputHomeFolder[0], $expectedHomeFolder[0]],
'set home folder, variant 2' => ['homeFolderNamingRule', $inputHomeFolder[1], $expectedHomeFolder[1]],
diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 0aac69f0d8ad1..324b33a7ac673 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -1437,14 +1437,6 @@
fileIsEncrypted]]>
-
-
-
-
-
-
-
-
diff --git a/core/Command/InterruptedException.php b/core/Command/InterruptedException.php
index 84b957c1e31a9..a43c34ec96102 100644
--- a/core/Command/InterruptedException.php
+++ b/core/Command/InterruptedException.php
@@ -13,5 +13,5 @@
/**
* Exception for when the user hit ctrl-c
*/
-class InterruptedException extends \Exception {
+class InterruptedException extends \OCP\Console\Exception\InterruptedException {
}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 8e2cdfc266db6..e4c6e9172b27a 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -338,8 +338,18 @@
'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\\Exception\\InterruptedException' => $baseDir . '/lib/public/Console/Exception/InterruptedException.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\\ISignalHandler' => $baseDir . '/lib/public/Console/ISignalHandler.php',
+ 'OCP\\Console\\OutputFormat' => $baseDir . '/lib/public/Console/OutputFormat.php',
'OCP\\Console\\ReservedOptions' => $baseDir . '/lib/public/Console/ReservedOptions.php',
+ 'OCP\\Console\\Verbosity' => $baseDir . '/lib/public/Console/Verbosity.php',
'OCP\\Constants' => $baseDir . '/lib/public/Constants.php',
'OCP\\Contacts\\ContactsMenu\\IAction' => $baseDir . '/lib/public/Contacts/ContactsMenu/IAction.php',
'OCP\\Contacts\\ContactsMenu\\IActionFactory' => $baseDir . '/lib/public/Contacts/ContactsMenu/IActionFactory.php',
@@ -1374,6 +1384,12 @@
'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php',
'OC\\Config\\UserConfigEntry' => $baseDir . '/lib/private/Config/UserConfigEntry.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\\NullOutput' => $baseDir . '/lib/private/Console/NullOutput.php',
+ 'OC\\Console\\OutputAdapter' => $baseDir . '/lib/private/Console/OutputAdapter.php',
+ 'OC\\Console\\ReflectionMember' => $baseDir . '/lib/private/Console/ReflectionMember.php',
+ 'OC\\Console\\SignalHandlerAdapter' => $baseDir . '/lib/private/Console/SignalHandlerAdapter.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 5ae915263d482..c909123b64889 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -379,8 +379,18 @@ 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\\Exception\\InterruptedException' => __DIR__ . '/../../..' . '/lib/public/Console/Exception/InterruptedException.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\\ISignalHandler' => __DIR__ . '/../../..' . '/lib/public/Console/ISignalHandler.php',
+ 'OCP\\Console\\OutputFormat' => __DIR__ . '/../../..' . '/lib/public/Console/OutputFormat.php',
'OCP\\Console\\ReservedOptions' => __DIR__ . '/../../..' . '/lib/public/Console/ReservedOptions.php',
+ 'OCP\\Console\\Verbosity' => __DIR__ . '/../../..' . '/lib/public/Console/Verbosity.php',
'OCP\\Constants' => __DIR__ . '/../../..' . '/lib/public/Constants.php',
'OCP\\Contacts\\ContactsMenu\\IAction' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IAction.php',
'OCP\\Contacts\\ContactsMenu\\IActionFactory' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IActionFactory.php',
@@ -1415,6 +1425,12 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php',
'OC\\Config\\UserConfigEntry' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfigEntry.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\\NullOutput' => __DIR__ . '/../../..' . '/lib/private/Console/NullOutput.php',
+ 'OC\\Console\\OutputAdapter' => __DIR__ . '/../../..' . '/lib/private/Console/OutputAdapter.php',
+ 'OC\\Console\\ReflectionMember' => __DIR__ . '/../../..' . '/lib/private/Console/ReflectionMember.php',
+ 'OC\\Console\\SignalHandlerAdapter' => __DIR__ . '/../../..' . '/lib/private/Console/SignalHandlerAdapter.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 e9b638a36e752..7f903693d1cf7 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->addCommand(new CommandAdapter($command, null, \OC::$server));
+ continue;
+ }
+
+ $hasMethodCommands = false;
+ foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
+ if ($reflectionMethod->getAttributes(AsCommand::class) !== []) {
+ $this->application->addCommand(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..c8809fcb2288f
--- /dev/null
+++ b/lib/private/Console/CommandAdapter.php
@@ -0,0 +1,294 @@
+ */
+ 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 {
+ $this->setHelp('More extensive and thorough documentation may be found at ' . Server::get(Defaults::class)->getDocBaseUrl() . PHP_EOL);
+
+ if ($this->asCommand->supportsOutputFormat) {
+ $this->addOption(
+ 'output',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Output format (plain, json or json_pretty, default is plain)',
+ $this->defaultOutputFormat
+ );
+ }
+
+ $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();
+ $default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null;
+
+ if ($type instanceof \ReflectionUnionType) {
+ // A union of bool with string/int/float declares an option whose value
+ // is itself optional, e.g. "--foo" (true), "--foo=bar" (bar) or omitted (false)
+ $typeNames = array_map(
+ static fn (\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null,
+ $type->getTypes(),
+ );
+ sort($typeNames);
+ $unionTypeName = implode('|', array_filter($typeNames));
+
+ if (!\in_array($unionTypeName, self::OPTION_VALUE_OPTIONAL_UNION_TYPES, true)) {
+ throw new \LogicException(\sprintf('The union type for option "$%s" of "%s" is not supported as a command option. Only "%s" types are allowed.', $name, $reflection->getSourceName(), implode('", "', self::OPTION_VALUE_OPTIONAL_UNION_TYPES)));
+ }
+
+ if ($default !== false) {
+ throw new \LogicException(\sprintf('The option "$%s" of "%s" must have a default value of false.', $name, $reflection->getSourceName()));
+ }
+
+ $this->addOption($option->name, $option->shortcut, InputOption::VALUE_OPTIONAL, $option->description, $default);
+ continue;
+ }
+
+ if (!$type instanceof \ReflectionNamedType) {
+ throw new \LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped or Intersection types are not supported for command options.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
+ }
+
+ $allowNull = $reflection->isNullable();
+ $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);
+
+ $symfonyStyle = new SymfonyStyle($input, $output);
+
+ $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])) {
+ $value = $input->getOption($this->options[$name]['option']->name);
+ if ($value === null && $parameter->getType() instanceof \ReflectionUnionType) {
+ // The option was passed without a value, e.g. "--foo"
+ $value = true;
+ }
+ $parameters[] = $value;
+ continue;
+ }
+
+ $type = $parameter->getType();
+ if ($type instanceof \ReflectionNamedType && $type->getName() === IOutput::class) {
+ $parameters[] = new OutputAdapter($output, $input, $symfonyStyle, $this);
+ continue;
+ }
+
+ if ($type instanceof \ReflectionNamedType && $type->getName() === IInput::class) {
+ $parameters[] = new InputAdapter($input, $symfonyStyle);
+ continue;
+ }
+
+ if ($type instanceof \ReflectionNamedType && $type->getName() === ISignalHandler::class) {
+ $parameters[] = new SignalHandlerAdapter($this);
+ continue;
+ }
+
+ if ($type instanceof \ReflectionNamedType && $type->getName() === OutputFormat::class) {
+ $output = OutputFormat::tryFrom($input->getOption('output'));
+ if ($output === null) {
+ $output = OutputFormat::Plain;
+ }
+ $parameters[] = $output;
+ continue;
+ }
+
+ throw new \LogicException(\sprintf('Unable to resolve parameter "$%s" of "%s": it is neither an #[Argument], an #[Option], nor an %s, %s, %s or %s.', $name, $this->reflectionMethod->getName(), IOutput::class, IInput::class, ISignalHandler::class, OutputFormat::class));
+ }
+
+ if ($this->method !== null) {
+ $result = $instance->{$this->method}(...$parameters);
+ } else {
+ $result = $instance(...$parameters);
+ }
+
+ return $result instanceof ExitCode ? $result->value : $result;
+ }
+
+ #[Override]
+ public function abortIfInterrupted(): void {
+ // To make it public
+ parent::abortIfInterrupted();
+ }
+
+ #[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..24f60338b1d82
--- /dev/null
+++ b/lib/private/Console/InputAdapter.php
@@ -0,0 +1,71 @@
+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);
+ }
+
+ #[Override]
+ public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed {
+ return $this->symfonyStyle->ask($question, $default, $validator);
+ }
+
+ #[Override]
+ public function askHidden(string $question, ?callable $validator = null): mixed {
+ return $this->symfonyStyle->askHidden($question, $validator);
+ }
+
+ #[Override]
+ public function confirm(string $question, bool $default = true): bool {
+ return $this->symfonyStyle->confirm($question, $default);
+ }
+
+ #[Override]
+ public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed {
+ return $this->symfonyStyle->choice($question, $choices, $default, $multiSelect);
+ }
+}
diff --git a/lib/private/Console/NullOutput.php b/lib/private/Console/NullOutput.php
new file mode 100644
index 0000000000000..5a9443d946b72
--- /dev/null
+++ b/lib/private/Console/NullOutput.php
@@ -0,0 +1,84 @@
+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());
+ }
+
+ #[Override]
+ public function progressStart(int $max = 0): void {
+ $this->symfonyStyle->progressStart($max);
+ }
+
+ #[Override]
+ public function progressAdvance(int $step = 1): void {
+ $this->symfonyStyle->progressAdvance($step);
+ }
+
+ #[Override]
+ public function progressFinish(): void {
+ $this->symfonyStyle->progressFinish();
+ }
+
+ #[Override]
+ public function progressIterate(iterable $iterable, ?int $max = null): iterable {
+ return $this->symfonyStyle->progressIterate($iterable, $max);
+ }
+}
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/private/Console/SignalHandlerAdapter.php b/lib/private/Console/SignalHandlerAdapter.php
new file mode 100644
index 0000000000000..2f4e9676f62c0
--- /dev/null
+++ b/lib/private/Console/SignalHandlerAdapter.php
@@ -0,0 +1,23 @@
+commandAdapter->abortIfInterrupted();
+ }
+}
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..01d09523f931e
--- /dev/null
+++ b/lib/public/Console/Attribute/AsCommand.php
@@ -0,0 +1,79 @@
+ 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.
+ *
+ * To declare an option whose value is itself optional (e.g. "--foo" alone returns
+ * `true`, "--foo=bar" returns `'bar'`, and omitting the option returns `false`),
+ * type the parameter as a union of `bool` with `string`, `int` or `float`
+ * (e.g. `string|bool $foo = false`).
+ *
+ * @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/Exception/InterruptedException.php b/lib/public/Console/Exception/InterruptedException.php
new file mode 100644
index 0000000000000..f7e5e0c2fa8d3
--- /dev/null
+++ b/lib/public/Console/Exception/InterruptedException.php
@@ -0,0 +1,16 @@
+
+ * @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;
+
+ /**
+ * Asks the user to provide some value.
+ *
+ * ```
+ * $input->ask('What is your name?');
+ * ```
+ *
+ * You can pass the default value as the second argument so the user can hit the key to select that value:
+ *
+ * ```
+ * $input->ask('Where are you from?', 'United States');
+ * ```
+ *
+ * In case you need to validate the given value, pass a callback validator as the third argument:
+ *
+ * ```
+ * $input->ask('Number of workers to start', '1', function (string $number): int {
+ * if (!is_numeric($number)) {
+ * throw new \RuntimeException('You must type a number.');
+ * }
+ *
+ * return (int) $number;
+ * });
+ * ```
+ *
+ * @param callable(string):mixed|null $validator
+ * @since 35.0.0
+ */
+ public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed;
+
+ /**
+ * Ask the user to provide some value but the user's input will be hidden, and it cannot define a default value.
+ *
+ * Use it when asking for sensitive information:
+ *
+ * ```
+ * $input->askHidden('What is your password?');
+ * ```
+ *
+ * In case you need to validate the given value, pass a callback validator as the second argument:
+ *
+ * ```
+ * $input->askHidden('What is your password?', function (string $password): string {
+ * if (empty($password)) {
+ * throw new \RuntimeException('Password cannot be empty.');
+ * }
+ *
+ * return $password;
+ * });
+ * ```
+ *
+ * @param callable(string):mixed|null $validator
+ * @since 35.0.0
+ */
+ public function askHidden(string $question, ?callable $validator = null): mixed;
+
+ /**
+ * Ask a Yes/No question to the user, and it only returns true or false:
+ *
+ * ```
+ * $input->confirm('Restart the web server?');
+ * ```
+ *
+ * You can pass the default value as the second argument so the user can hit the key to select that value:
+ *
+ * ```
+ * $input->confirm('Restart the web server?', true);
+ * ```
+ *
+ * @param string $question
+ * @since 35.0.0
+ */
+ public function confirm(string $question, bool $default = true): bool;
+
+ /**
+ * Ask a question whose answer is constrained to the given list of valid answers:
+ *
+ * ```
+ * $input->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3']);
+ * ```
+ *
+ * You can pass the default value as the third argument so the user can hit the key to select that value:
+ *
+ * ```
+ * $input->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], 'queue1');
+ * ```
+ *
+ * Choice questions display both the choice value and a numeric index, which starts from 0 by default. To use custom indices, pass an array with custom numeric keys as the choice values:
+ *
+ * ```
+ * $input->choice('Select the queue to analyze', [5 => 'queue1', 6 => 'queue2', 7 => 'queue3']);
+ * ```
+ *
+ * Finally, you can allow users to select multiple choices. To do so, users must separate each choice with a comma (e.g. typing 1, 2 will select choice 1 and 2):
+ *
+ * ```
+ * $input->choice('Select the queue to analyze', ['queue1', 'queue2', 'queue3'], multiSelect: true);
+ * ```
+ *
+ * @param array $choices
+ * @since 35.0.0
+ */
+ public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed;
+}
diff --git a/lib/public/Console/IOutput.php b/lib/public/Console/IOutput.php
new file mode 100644
index 0000000000000..5629caa86b1d8
--- /dev/null
+++ b/lib/public/Console/IOutput.php
@@ -0,0 +1,158 @@
+> $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;
+
+ /**
+ * Displays a progress bar with a number of steps equal to the argument passed to the method (don't pass any value if the length of the progress bar is unknown):
+ *
+ * ```
+ * // displays a progress bar of unknown length
+ * $output->progressStart();
+ * ```
+ *
+ * ```
+ * // displays a 100-step length progress bar
+ * $output->progressStart(100);
+ * ```
+ * @since 35.0.0
+ */
+ public function progressStart(int $max = 0): void;
+
+ /**
+ * Make the progress bar advance the given number of steps (or 1 step if no argument is passed):
+ *
+ * ```
+ * // advances the progress bar 1 step
+ * $output->progressAdvance();
+ * ```
+ *
+ * ```
+ * // advances the progress bar 10 steps
+ * $output->progressAdvance(10);
+ * ```
+ * @since 35.0.0
+ */
+ public function progressAdvance(int $step = 1): void;
+
+ /**
+ * Finish the progress bar (filling up all the remaining steps when its length is known):
+ *
+ * ```
+ * $output->progressFinish();
+ * ```
+ * @since 35.0.0
+ */
+ public function progressFinish(): void;
+
+ /**
+ * If your progress bar loops over an iterable collection, use the progressIterate() helper:
+ *
+ * ```
+ * $iterable = [1, 2];
+ *
+ * foreach ($output->progressIterate($iterable) as $value) {
+ * // ... do some work
+ * }
+ * ```
+ * @template TKey
+ * @template TValue
+ *
+ * @param iterable $iterable
+ * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
+ *
+ * @return iterable
+ * @since 35.0.0
+ */
+ public function progressIterate(iterable $iterable, ?int $max = null): iterable;
+}
diff --git a/lib/public/Console/ISignalHandler.php b/lib/public/Console/ISignalHandler.php
new file mode 100644
index 0000000000000..09dc56a8eda61
--- /dev/null
+++ b/lib/public/Console/ISignalHandler.php
@@ -0,0 +1,47 @@
+items() as $item) {
+ * $signalHandler->abortIfInterrupted();
+ * $this->process($item);
+ * }
+ * } catch (InterruptedException) {
+ * $output->writeln('Interrupted by user');
+ * return ExitCode::Failure;
+ * }
+ *
+ * return ExitCode::Success;
+ * }
+ * }
+ * ```
+ *
+ * @since 35.0.0
+ */
+#[Consumable(since: '35.0.0')]
+interface ISignalHandler {
+ /**
+ * Throw when interrupted by user (Ctrl-C/SIGTERM)
+ *
+ * @throws InterruptedException
+ * @since 35.0.0
+ */
+ public function abortIfInterrupted(): void;
+}
diff --git a/lib/public/Console/OutputFormat.php b/lib/public/Console/OutputFormat.php
new file mode 100644
index 0000000000000..a9fcdb939b9a2
--- /dev/null
+++ b/lib/public/Console/OutputFormat.php
@@ -0,0 +1,40 @@
+