2 declare(strict_types = 1);
18 use Symfony\Component\Console\Command\Command;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\InputOption;
21 use Symfony\Component\Console\Output\OutputInterface;
22 use Symfony\Component\Console\Style\SymfonyStyle;
43 ->setDescription(
'Looking for files from TYPO3 managed records which are referenced more than once')
46 - a perfect integrity of the reference index table (always update the reference index table before using this tool!)
47 - files found in deleted records are included (otherwise you would see a false list of lost files)
49 Files attached to records in TYPO3 using a "group" type configuration in TCA or FlexForm DataStructure are managed exclusively by the system and there must always exist a 1-1 reference between the file and the reference in the record.
50 This tool will expose when such files are referenced from multiple locations which is considered an integrity error.
51 If a multi-reference is found it was typically created because the record was copied or modified outside of DataHandler which will otherwise maintain the relations correctly.
52 Multi-references should be resolved to 1-1 references as soon as possible. The danger of keeping multi-references is that if the file is removed from one of the referring records it will actually be deleted in the file system, leaving missing files for the remaining referers!
54 If the option "--dry-run" is not set, the files that are referenced multiple times are copied with a new name
55 and the references are updated accordingly.
56 Warning: First, make sure those files are not used somewhere TYPO3 does not know about!
58 If you want to get more detailed information, use the --verbose option.')
62 InputOption::VALUE_NONE,
63 'If this option is set, the files will not actually be deleted, but just the output which files would be deleted are shown'
68 InputOption::VALUE_NONE,
69 'Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode'
87 $io =
new SymfonyStyle($input,
$output);
88 $io->title($this->getDescription());
90 $dryRun = $input->hasOption(
'dry-run') && $input->getOption(
'dry-run') !=
false ? true :
false;
97 if (count($doubleFiles)) {
98 if (!$io->isQuiet()) {
99 $io->note(
'Found ' . count($doubleFiles) .
' files that are referenced more than once.');
100 if ($io->isVerbose()) {
101 $io->listing($doubleFiles);
106 $io->success(
'Cleaned up ' . count($doubleFiles) .
' files which have been referenced multiple times.');
108 $io->success(
'Nothing to do, no files found which are referenced more than once.');
124 $io->note(
'Finding files referenced multiple times in records managed by TYPO3 requires a clean reference index (sys_refindex)');
125 $updateReferenceIndex =
false;
126 if ($input->hasOption(
'update-refindex') && $input->getOption(
'update-refindex')) {
127 $updateReferenceIndex =
true;
128 } elseif ($input->isInteractive()) {
129 $updateReferenceIndex = $io->confirm(
'Should the reference index be updated right now?',
false);
133 if ($updateReferenceIndex) {
134 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
135 $referenceIndex->updateIndex(
false, !$io->isQuiet());
137 $io->writeln(
'Reference index is assumed to be up to date, continuing.');
148 $multipleReferencesList = [];
151 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
152 ->getQueryBuilderForTable(
'sys_refindex');
154 $result = $queryBuilder
156 ->from(
'sys_refindex')
158 $queryBuilder->expr()->eq(
'ref_table', $queryBuilder->createNamedParameter(
'_FILE', \PDO::PARAM_STR)),
159 $queryBuilder->expr()->eq(
'softref_key', $queryBuilder->createNamedParameter(
'', \PDO::PARAM_STR))
164 $allReferencesToFiles = [];
165 while ($record = $result->fetch()) {
168 $hash = $record[
'hash'];
169 $fileName = $record[
'ref_string'];
171 if (isset($allReferencesToFiles[$fileName])) {
172 if (!is_array($multipleReferencesList[$fileName])) {
173 $multipleReferencesList[$fileName] = [];
174 $multipleReferencesList[$fileName][$allReferencesToFiles[$fileName][
'hash']] = $allReferencesToFiles[$fileName][
'infoString'];
176 $multipleReferencesList[$fileName][$hash] = $infoString;
178 $allReferencesToFiles[$fileName] = [
179 'infoString' => $infoString,
197 $fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
198 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
200 foreach ($multipleReferencesToFiles as $fileName => $usages) {
201 $absoluteFileName = GeneralUtility::getFileAbsFileName($fileName);
202 if ($absoluteFileName && @is_file($absoluteFileName)) {
203 if ($io->isVeryVerbose()) {
204 $io->writeln(
'Processing file "' . $absoluteFileName .
'"');
207 foreach ($usages as $hash => $recReference) {
208 if ($counter++ === 0) {
209 $io->writeln(
'Keeping "' . $fileName .
'" for record "' . $recReference .
'"');
215 GeneralUtility::upload_copy_move($absoluteFileName, $newName);
217 if (@is_file($newName)) {
220 $io->error(
'ReferenceIndex::setReferenceValue() reported "' . $error .
'"');
223 $io->error(
'File "' . $newName .
'" could not be created.');
229 $io->error(
'File "' . $absoluteFileName .
'" was not found.');
242 return $record[
'tablename']
243 .
':' . $record[
'recuid']
244 .
':' . $record[
'field']
245 . ($record[
'flexpointer'] ?
':' . $record[
'flexpointer'] :
'')
246 . ($record[
'softref_key'] ?
':' . $record[
'softref_key'] .
' (Soft Reference) ' :
'')
247 . ($record[
'deleted'] ?
' (DELETED)' :
'');