diff options
Diffstat (limited to 'apps/files/lib')
-rw-r--r-- | apps/files/lib/Command/SanitizeFilenames.php | 50 | ||||
-rw-r--r-- | apps/files/lib/Command/TransferOwnership.php | 46 | ||||
-rw-r--r-- | apps/files/lib/Service/OwnershipTransferService.php | 79 | ||||
-rw-r--r-- | apps/files/lib/Settings/DeclarativeAdminSettings.php | 2 |
4 files changed, 125 insertions, 52 deletions
diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php index ea01afd20d6..a404f0b3fd9 100644 --- a/apps/files/lib/Command/SanitizeFilenames.php +++ b/apps/files/lib/Command/SanitizeFilenames.php @@ -27,7 +27,7 @@ use Symfony\Component\Console\Output\OutputInterface; class SanitizeFilenames extends Base { private OutputInterface $output; - private string $charReplacement; + private ?string $charReplacement; private bool $dryRun; public function __construct( @@ -43,10 +43,6 @@ class SanitizeFilenames extends Base { protected function configure(): void { parent::configure(); - $forbiddenCharacter = $this->filenameValidator->getForbiddenCharacters(); - $charReplacement = array_diff([' ', '_', '-'], $forbiddenCharacter); - $charReplacement = reset($charReplacement) ?: ''; - $this ->setName('files:sanitize-filenames') ->setDescription('Renames files to match naming constraints') @@ -65,16 +61,25 @@ class SanitizeFilenames extends Base { 'c', mode: InputOption::VALUE_REQUIRED, description: 'Replacement for invalid character (by default space, underscore or dash is used)', - default: $charReplacement, ); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->charReplacement = $input->getOption('char-replacement'); - if ($this->charReplacement === '' || mb_strlen($this->charReplacement) > 1) { - $output->writeln('<error>No character replacement given</error>'); - return 1; + // check if replacement is needed + $c = $this->filenameValidator->getForbiddenCharacters(); + if (count($c) > 0) { + try { + $this->filenameValidator->sanitizeFilename($c[0], $this->charReplacement); + } catch (\InvalidArgumentException) { + if ($this->charReplacement === null) { + $output->writeln('<error>Character replacement required</error>'); + } else { + $output->writeln('<error>Invalid character replacement given</error>'); + } + return 1; + } } $this->dryRun = $input->getOption('dry-run'); @@ -115,8 +120,8 @@ class SanitizeFilenames extends Base { try { $oldName = $node->getName(); - if (!$this->filenameValidator->isFilenameValid($oldName)) { - $newName = $this->sanitizeName($oldName); + $newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement); + if ($oldName !== $newName) { $newName = $folder->getNonExistingName($newName); $path = rtrim(dirname($node->getPath()), '/'); @@ -142,27 +147,4 @@ class SanitizeFilenames extends Base { } } - private function sanitizeName(string $name): string { - $l10n = $this->l10nFactory->get('files'); - - foreach ($this->filenameValidator->getForbiddenExtensions() as $extension) { - if (str_ends_with($name, $extension)) { - $name = substr($name, 0, strlen($name) - strlen($extension)); - } - } - - $basename = substr($name, 0, strpos($name, '.', 1) ?: null); - if (in_array($basename, $this->filenameValidator->getForbiddenBasenames())) { - $name = str_replace($basename, $l10n->t('%1$s (renamed)', [$basename]), $name); - } - - if ($name === '') { - $name = $l10n->t('renamed file'); - } - - $forbiddenCharacter = $this->filenameValidator->getForbiddenCharacters(); - $name = str_replace($forbiddenCharacter, $this->charReplacement, $name); - - return $name; - } } diff --git a/apps/files/lib/Command/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php index edc73e62c38..104a8fb4985 100644 --- a/apps/files/lib/Command/TransferOwnership.php +++ b/apps/files/lib/Command/TransferOwnership.php @@ -11,20 +11,26 @@ namespace OCA\Files\Command; use OCA\Files\Exception\TransferOwnershipException; use OCA\Files\Service\OwnershipTransferService; +use OCA\Files_External\Config\ConfigAdapter; +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 { public function __construct( private IUserManager $userManager, private OwnershipTransferService $transferService, private IConfig $config, + private IMountManager $mountManager, ) { parent::__construct(); } @@ -60,6 +66,16 @@ class TransferOwnership extends Command { InputOption::VALUE_OPTIONAL, 'transfer incoming user file shares to destination user. Usage: --transfer-incoming-shares=1 (value required)', '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', ); } @@ -87,6 +103,31 @@ class TransferOwnership extends Command { return self::FAILURE; } + $path = ltrim($input->getOption('path'), '/'); + $includeExternalStorage = $input->getOption('include-external-storage'); + if ($includeExternalStorage) { + $mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/')); + /** @var IMountPoint[] $mounts */ + $mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class); + if (count($mounts) > 0) { + $output->writeln(count($mounts) . ' external storages will be transferred:'); + foreach ($mounts as $mount) { + $output->writeln(' - <info>' . $mount->getMountPoint() . '</info>'); + } + $output->writeln(''); + $output->writeln('<comment>Any other users with access to these external storages will lose access to the files.</comment>'); + $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; + } + } + } + } + try { $includeIncomingArgument = $input->getOption('transfer-incoming-shares'); @@ -112,11 +153,12 @@ class TransferOwnership extends Command { $this->transferService->transfer( $sourceUserObject, $destinationUserObject, - ltrim($input->getOption('path'), '/'), + $path, $output, $input->getOption('move') === true, false, - $includeIncoming + $includeIncoming, + $includeExternalStorage, ); } catch (TransferOwnershipException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php index 7f6681a9b89..b3a36ee13e5 100644 --- a/apps/files/lib/Service/OwnershipTransferService.php +++ b/apps/files/lib/Service/OwnershipTransferService.php @@ -15,10 +15,11 @@ use OC\Files\View; use OC\User\NoUserException; use OCA\Encryption\Util; use OCA\Files\Exception\TransferOwnershipException; +use OCA\Files_External\Config\ConfigAdapter; use OCP\Encryption\IManager as IEncryptionManager; +use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IUserMountCache; use OCP\Files\FileInfo; -use OCP\Files\IHomeStorage; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountManager; @@ -70,6 +71,7 @@ class OwnershipTransferService { bool $move = false, bool $firstLogin = false, bool $transferIncomingShares = false, + bool $includeExternalStorage = false, ): void { $output = $output ?? new NullOutput(); $sourceUid = $sourceUser->getUID(); @@ -149,7 +151,8 @@ class OwnershipTransferService { $sourcePath, $finalTarget, $view, - $output + $output, + $includeExternalStorage, ); $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize(); @@ -215,11 +218,14 @@ class OwnershipTransferService { * * @throws TransferOwnershipException */ - protected function analyse(string $sourceUid, + protected function analyse( + string $sourceUid, string $destinationUid, string $sourcePath, View $view, - OutputInterface $output): void { + OutputInterface $output, + bool $includeExternalStorage = false, + ): void { $output->writeln('Validating quota'); $sourceFileInfo = $view->getFileInfo($sourcePath, false); if ($sourceFileInfo === false) { @@ -247,17 +253,22 @@ class OwnershipTransferService { $encryptedFiles[] = $sourceFileInfo; } else { $this->walkFiles($view, $sourcePath, - function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles) { + function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) { if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) { + $mount = $fileInfo->getMountPoint(); // only analyze into folders from main storage, - if (!$fileInfo->getStorage()->instanceOfStorage(IHomeStorage::class)) { + if ( + $mount->getMountProvider() instanceof IHomeMountProvider || + ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter) + ) { + if ($fileInfo->isEncrypted()) { + /* Encrypted folder means e2ee encrypted, we cannot transfer it */ + $encryptedFiles[] = $fileInfo; + } + return true; + } else { return false; } - if ($fileInfo->isEncrypted()) { - /* Encrypted folder means e2ee encrypted, we cannot transfer it */ - $encryptedFiles[] = $fileInfo; - } - return true; } $progress->advance(); if ($fileInfo->isEncrypted() && !$masterKeyEnabled) { @@ -399,11 +410,14 @@ class OwnershipTransferService { /** * @throws TransferOwnershipException */ - protected function transferFiles(string $sourceUid, + protected function transferFiles( + string $sourceUid, string $sourcePath, string $finalTarget, View $view, - OutputInterface $output): void { + OutputInterface $output, + bool $includeExternalStorage, + ): void { $output->writeln("Transferring files to $finalTarget ..."); // This change will help user to transfer the folder specified using --path option. @@ -412,15 +426,50 @@ class OwnershipTransferService { $view->mkdir($finalTarget); $finalTarget = $finalTarget . '/' . basename($sourcePath); } - if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) { - throw new TransferOwnershipException('Could not transfer files.', 1); + $sourceInfo = $view->getFileInfo($sourcePath); + + /// handle the external storages mounted at the root, or the admin specifying an external storage with --path + if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) { + $this->moveMountContents($view, $sourcePath, $finalTarget); + } else { + if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) { + throw new TransferOwnershipException('Could not transfer files.', 1); + } + } + + if ($includeExternalStorage) { + $nestedMounts = $this->mountManager->findIn($sourcePath); + foreach ($nestedMounts as $mount) { + if ($mount->getMountProvider() === ConfigAdapter::class) { + $relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath)); + $this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath); + } + } } + if (!is_dir("$sourceUid/files")) { // because the files folder is moved away we need to recreate it $view->mkdir("$sourceUid/files"); } } + private function moveMountContents(View $rootView, string $source, string $target) { + if ($rootView->copy($source, $target)) { + // just doing `rmdir` on the mountpoint would cause it to try and unmount the storage + // we need to empty the contents instead + $content = $rootView->getDirectoryContent($source); + foreach ($content as $item) { + if ($item->getType() === FileInfo::TYPE_FOLDER) { + $rootView->rmdir($item->getPath()); + } else { + $rootView->unlink($item->getPath()); + } + } + } else { + throw new TransferOwnershipException("Could not transfer $source to $target"); + } + } + /** * @param string $targetLocation New location of the transfered node * @param array<array{share: IShare, suffix: string}> $shares previously collected share information diff --git a/apps/files/lib/Settings/DeclarativeAdminSettings.php b/apps/files/lib/Settings/DeclarativeAdminSettings.php index 2f363f05958..bbf97cc4d32 100644 --- a/apps/files/lib/Settings/DeclarativeAdminSettings.php +++ b/apps/files/lib/Settings/DeclarativeAdminSettings.php @@ -49,7 +49,7 @@ class DeclarativeAdminSettings implements IDeclarativeSettingsFormWithHandlers { 'doc_url' => $this->urlGenerator->linkToDocs('admin-windows-compatible-filenames'), 'description' => ( $this->l->t('Allow to restrict filenames to ensure files can be synced with all clients. By default all filenames valid on POSIX (e.g. Linux or macOS) are allowed.') - . "\n" . $this->l->t('After enabling the windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner.') + . "\n" . $this->l->t('After enabling the Windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner.') . "\n" . $this->l->t('It is also possible to migrate files automatically after enabling this setting, please refer to the documentation about the occ command.') ), |