filenameValidator->getForbiddenCharacters(); $charReplacement = array_diff([' ', '_', '-'], $forbiddenCharacter); $charReplacement = reset($charReplacement) ?: ''; $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)', 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('No 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(); if (!$this->filenameValidator->isFilenameValid($oldName)) { $newName = $this->sanitizeName($oldName); $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); } } } 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; } }