1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
|
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Command;
use Exception;
use OC\Core\Command\Base;
use OC\Files\FilenameValidator;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IUser;
use OCP\IUserManager;
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 {
private OutputInterface $output;
private ?string $charReplacement;
private bool $dryRun;
public function __construct(
private IUserManager $userManager,
private IRootFolder $rootFolder,
private IUserSession $session,
private IFactory $l10nFactory,
private FilenameValidator $filenameValidator,
) {
parent::__construct();
}
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)',
);
}
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('<error>Character replacement required</error>');
} else {
$output->writeln('<error>Invalid character replacement given</error>');
}
return 1;
}
}
$this->dryRun = $input->getOption('dry-run');
if ($this->dryRun) {
$output->writeln('<info>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("<error>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('<info>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('<comment>skipping: ' . $node->getPath() . ' (file is locked)</>');
} catch (NotPermittedException) {
$this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (no permissions)</>');
} catch (Exception) {
$this->output->writeln('<error>failed: ' . $node->getPath() . '</>');
}
if ($node instanceof Folder) {
$this->sanitizeFiles($node);
}
}
}
}
|