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)', ); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->charReplacement = $input->getOption('char-replacement'); // 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('Character replacement required'); } else { $output->writeln('Invalid character replacement given'); } return 1; } } $this->dryRun = $input->getOption('dry-run'); 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) { $user = $this->userManager->get($userId); if ($user === null) { $output->writeln("User '$userId' does not exist - skipping"); continue; } $this->sanitizeUserFiles($user); } } else { $this->userManager->callForSeenUsers($this->sanitizeUserFiles(...)); } return self::SUCCESS; } private function sanitizeUserFiles(IUser $user): void { // Set an active user so that event listeners can correctly work (e.g. files versions) $this->session->setVolatileActiveUser($user); $this->output->writeln('Analyzing files of ' . $user->getUID() . ''); $folder = $this->rootFolder->getUserFolder($user->getUID()); $this->sanitizeFiles($folder); } private function sanitizeFiles(Folder $folder): void { foreach ($folder->getDirectoryListing() as $node) { $this->output->writeln('scanning: ' . $node->getPath(), OutputInterface::VERBOSITY_VERBOSE); try { $oldName = $node->getName(); $newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement); if ($oldName !== $newName) { $newName = $folder->getNonExistingName($newName); $path = rtrim(dirname($node->getPath()), '/'); if (!$this->dryRun) { $node->move("$path/$newName"); } elseif (!$folder->isCreatable()) { // simulate error for dry run throw new NotPermittedException(); } $this->output->writeln('renamed: "' . $oldName . '" to "' . $newName . '"'); } } catch (LockedException) { $this->output->writeln('skipping: ' . $node->getPath() . ' (file is locked)'); } catch (NotPermittedException) { $this->output->writeln('skipping: ' . $node->getPath() . ' (no permissions)'); } catch (Exception) { $this->output->writeln('failed: ' . $node->getPath() . ''); } if ($node instanceof Folder) { $this->sanitizeFiles($node); } } } }