Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 48 additions & 52 deletions apps/files/lib/Command/Copy.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,99 +9,96 @@
namespace OCA\Files\Command;

use OC\Core\Command\Info\FileUtils;
use OCP\Console\Attribute\Argument;
use OCP\Console\Attribute\AsCommand;
use OCP\Console\Attribute\Option;
use OCP\Console\ExitCode;
use OCP\Console\IOutput;
use OCP\Console\IQuestionHelper;
use OCP\Files\Folder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

class Copy extends Command {
#[AsCommand(
name: 'files:copy',
description: 'Copy a file or folder',
)]
class Copy {
public function __construct(
private FileUtils $fileUtils,
private readonly FileUtils $fileUtils,
) {
parent::__construct();
}

#[\Override]
protected function configure(): void {
$this
->setName('files:copy')
->setDescription('Copy a file or folder')
->addArgument('source', InputArgument::REQUIRED, 'Source file id or path')
->addArgument('target', InputArgument::REQUIRED, 'Target path')
->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for confirmation and don't output any warnings")
->addOption('no-target-directory', 'T', InputOption::VALUE_NONE, 'When target path is folder, overwrite the folder instead of copying into the folder');
}

#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
$sourceInput = $input->getArgument('source');
$targetInput = $input->getArgument('target');
$force = $input->getOption('force');
$noTargetDir = $input->getOption('no-target-directory');

$node = $this->fileUtils->getNode($sourceInput);
$targetNode = $this->fileUtils->getNode($targetInput);
public function __invoke(
IOutput $output,
IQuestionHelper $questionHelper,
#[Argument(description: 'Source file id or path')] string $source,
#[Argument(description: 'Target path')] string $target,
#[Option(
description: "Don't ask for confirmation and don't output any warnings",
shortcut: 'f',
)] bool $force = false,
#[Option(
name: 'no-target-directory',
description: 'When target path is folder, overwrite the folder instead of copying into the folder',
shortcut: 'T',
)] bool $noTargetDirectory = false,
): ExitCode {
$node = $this->fileUtils->getNode($source);
$targetNode = $this->fileUtils->getNode($target);

if (!$node) {
$output->writeln("<error>file $sourceInput not found</error>");
return 1;
$output->writeln("<error>file $source not found</error>");
return ExitCode::Failure;
}

$targetParentPath = dirname(rtrim($targetInput, '/'));
$targetParentPath = dirname(rtrim($target, '/'));
$targetParent = $this->fileUtils->getNode($targetParentPath);
if (!$targetParent) {
$output->writeln("<error>Target parent path $targetParentPath doesn't exist</error>");
return 1;
return ExitCode::Failure;
}

$wouldRequireDelete = false;

if ($targetNode) {
if (!$targetNode->isUpdateable()) {
$output->writeln("<error>$targetInput isn't writable</error>");
return 1;
$output->writeln("<error>$target isn't writable</error>");
return ExitCode::Failure;
}

if ($targetNode instanceof Folder) {
if ($noTargetDir) {
if ($noTargetDirectory) {
if (!$force) {
$output->writeln("Warning: <info>$sourceInput</info> is a file, but <info>$targetInput</info> is a folder");
$output->writeln("Warning: <info>$source</info> is a file, but <info>$target</info> 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: <info>$sourceInput</info> is a folder, but <info>$targetInput</info> is a file");
$output->writeln("Warning: <info>$source</info> is a folder, but <info>$target</info> is a file");
}
$wouldRequireDelete = true;
}
}

if ($wouldRequireDelete && $targetNode->getInternalPath() === '') {
$output->writeln("<error>Mount root can't be overwritten with a different type</error>");
return 1;
return ExitCode::Failure;
}

if ($wouldRequireDelete && !$targetNode->isDeletable()) {
$output->writeln("<error>$targetInput can't be deleted to be replaced with $sourceInput</error>");
return 1;
$output->writeln("<error>$target can't be deleted to be replaced with $source</error>");
return ExitCode::Failure;
}

if (!$force && $targetNode) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');

$question = new ConfirmationQuestion('<info>' . $targetInput . '</info> already exists, overwrite? [y/N] ', false);
if (!$helper->ask($input, $output, $question)) {
return 1;
$question = new ConfirmationQuestion('<info>' . $target . '</info> already exists, overwrite? [y/N] ', false);
if (!$questionHelper->ask($question)) {
return ExitCode::Failure;
}
}
}
Expand All @@ -110,9 +107,8 @@ public function execute(InputInterface $input, OutputInterface $output): int {
$targetNode->delete();
}

$node->copy($targetInput);
$node->copy($target);

return 0;
return ExitCode::Success;
}

}
70 changes: 33 additions & 37 deletions apps/files/lib/Command/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,58 +11,54 @@
use OC\Core\Command\Info\FileUtils;
use OCA\Files_Sharing\SharedStorage;
use OCA\Files_Trashbin\Trash\ITrashManager;
use OCP\Console\Attribute\Argument;
use OCP\Console\Attribute\AsCommand;
use OCP\Console\Attribute\Option;
use OCP\Console\ExitCode;
use OCP\Console\IOutput;
use OCP\Console\IQuestionHelper;
use OCP\Files\Folder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

class Delete extends Command {
#[AsCommand(
name: 'files:delete',
description: 'Delete a file or folder',
)]
class Delete {
public function __construct(
private readonly FileUtils $fileUtils,
private readonly ?ITrashManager $trashManager = null,
) {
parent::__construct();
}

#[\Override]
protected function configure(): void {
$this
->setName('files:delete')
->setDescription('Delete a file or folder')
->addArgument('file', InputArgument::REQUIRED, 'File id or path')
->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings")
->addOption('skip-trash', null, InputOption::VALUE_NONE, 'Bypass the trashbin when deleting the file or folder');
}

#[\Override]
public function execute(InputInterface $input, OutputInterface $output): int {
$fileInput = $input->getArgument('file');
$inputIsId = is_numeric($fileInput);
$force = $input->getOption('force');
$skipTrash = $input->getOption('skip-trash');
$node = $this->fileUtils->getNode($fileInput);
public function __invoke(
IOutput $output,
IQuestionHelper $questionHelper,
#[Argument(description: 'File id or path')] string $file,
#[Option(
description: "Don't ask for configuration and don't output any warnings",
shortcut: 'f',
)] bool $force = false,
#[Option(name: 'skip-trash', description: 'Bypass the trashbin when deleting the file or folder')] bool $skipTrash = false,
): ExitCode {
$inputIsId = is_numeric($file);
$node = $this->fileUtils->getNode($file);

if (!$node) {
$output->writeln("<error>file $fileInput not found</error>");
return self::FAILURE;
$output->writeln("<error>file $file not found</error>");
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("<info>$fileInput</info> in a shared file, do you want to unshare the file from <info>$user</info> instead of deleting the source file? [Y/n] ", true);
if ($helper->ask($input, $output, $question)) {
[,$user] = explode('/', $file, 3);
$question = new ConfirmationQuestion("<info>$file</info> in a shared file, do you want to unshare the file from <info>$user</info> instead of deleting the source file? [Y/n] ", true);
if ($questionHelper->ask($question)) {
$storage->unshareStorage();
return self::SUCCESS;
return ExitCode::Success;
} else {
$node = $storage->getShare()->getNode();
$output->writeln('');
Expand All @@ -76,8 +72,8 @@ public function execute(InputInterface $input, OutputInterface $output): int {
$output->writeln('');
foreach ($filesByUsers as $user => $filesByUser) {
$output->writeln($user . ':');
foreach ($filesByUser as $file) {
$output->writeln(' - ' . $file->getPath());
foreach ($filesByUser as $userFile) {
$output->writeln(' - ' . $userFile->getPath());
}
}
$output->writeln('');
Expand All @@ -89,7 +85,7 @@ public function execute(InputInterface $input, OutputInterface $output): int {
$maybeContents = '';
}
$question = new ConfirmationQuestion('Delete ' . $node->getPath() . $maybeContents . '? [y/N] ', false);
$deleteConfirmed = $helper->ask($input, $output, $question);
$deleteConfirmed = $questionHelper->ask($question);
}

if ($deleteConfirmed) {
Expand All @@ -104,6 +100,6 @@ public function execute(InputInterface $input, OutputInterface $output): int {
}
}

return self::SUCCESS;
return ExitCode::Success;
}
}
40 changes: 20 additions & 20 deletions apps/files/lib/Command/DeleteOrphanedFiles.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
Expand All @@ -8,41 +10,39 @@

namespace OCA\Files\Command;

use OCP\Console\Attribute\AsCommand;
use OCP\Console\Attribute\Option;
use OCP\Console\ExitCode;
use OCP\Console\IOutput;
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;

/**
* Delete all file entries that have no matching entries in the storage table.
*/
class DeleteOrphanedFiles extends Command {
#[AsCommand(
name: 'files:cleanup',
description: 'Clean up orphaned filecache and mount entries',
help: 'Deletes orphaned filecache and mount entries (those without an existing storage).',
)]
class DeleteOrphanedFiles {
public const CHUNK_SIZE = 200;

public function __construct(
protected IDBConnection $connection,
protected readonly IDBConnection $connection,
) {
parent::__construct();
}

#[\Override]
protected function configure(): void {
$this
->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);
}
Expand All @@ -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 {
Expand Down
Loading
Loading